fullstackgtm 0.44.0 → 0.46.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/CHANGELOG.md +293 -1
- package/INSTALL_FOR_AGENTS.md +13 -11
- package/README.md +45 -24
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/backfill.d.ts +86 -0
- package/dist/backfill.js +251 -0
- package/dist/bin.js +4 -1
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +82 -0
- package/dist/cli/auth.js +607 -0
- package/dist/cli/backfill.d.ts +1 -0
- package/dist/cli/backfill.js +125 -0
- package/dist/cli/backfillRuns.d.ts +1 -0
- package/dist/cli/backfillRuns.js +187 -0
- package/dist/cli/call.d.ts +1 -0
- package/dist/cli/call.js +387 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +806 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +346 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +646 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +456 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +71 -0
- package/dist/cli/shared.js +342 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +142 -0
- package/dist/cli/ui.js +427 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +123 -5157
- package/dist/connector.d.ts +23 -0
- package/dist/connector.js +47 -0
- package/dist/connectors/hubspot.d.ts +10 -1
- package/dist/connectors/hubspot.js +244 -10
- package/dist/connectors/hubspotAuth.js +5 -1
- package/dist/connectors/linkedin.js +14 -14
- package/dist/connectors/salesforce.d.ts +10 -1
- package/dist/connectors/salesforce.js +39 -6
- package/dist/connectors/signalSources.js +7 -59
- package/dist/connectors/stripe.d.ts +37 -0
- package/dist/connectors/stripe.js +103 -31
- package/dist/enrich.d.ts +7 -0
- package/dist/enrich.js +7 -0
- package/dist/health.d.ts +11 -69
- package/dist/health.js +4 -134
- package/dist/healthScore.d.ts +71 -0
- package/dist/healthScore.js +143 -0
- package/dist/icp.d.ts +15 -11
- package/dist/icp.js +19 -36
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +206 -0
- package/dist/market.d.ts +39 -1
- package/dist/market.js +116 -44
- package/dist/marketClassify.d.ts +11 -1
- package/dist/marketClassify.js +17 -2
- package/dist/marketTaxonomy.d.ts +6 -1
- package/dist/marketTaxonomy.js +4 -2
- package/dist/mcp-bin.js +31 -2
- package/dist/mcp.js +372 -166
- package/dist/progress.d.ts +96 -0
- package/dist/progress.js +142 -0
- package/dist/runReport.d.ts +24 -0
- package/dist/runReport.js +139 -4
- package/dist/schedule.d.ts +80 -2
- package/dist/schedule.js +254 -1
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/types.d.ts +29 -1
- package/docs/api.md +75 -8
- package/docs/architecture.md +2 -0
- package/docs/linkedin-connector-spec.md +1 -1
- package/docs/signal-spool-format.md +13 -0
- package/llms.txt +18 -9
- package/package.json +10 -3
- package/skills/fullstackgtm/SKILL.md +2 -1
- package/src/audit.ts +27 -1
- package/src/backfill.ts +340 -0
- package/src/bin.ts +5 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +669 -0
- package/src/cli/backfill.ts +156 -0
- package/src/cli/backfillRuns.ts +198 -0
- package/src/cli/call.ts +414 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +908 -0
- package/src/cli/fix.ts +373 -0
- package/src/cli/help.ts +702 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +524 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +392 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +497 -0
- package/src/cli.ts +122 -5855
- package/src/connector.ts +66 -0
- package/src/connectors/hubspot.ts +273 -9
- package/src/connectors/hubspotAuth.ts +5 -1
- package/src/connectors/linkedin.ts +14 -14
- package/src/connectors/salesforce.ts +51 -3
- package/src/connectors/signalSources.ts +7 -56
- package/src/connectors/stripe.ts +154 -34
- package/src/enrich.ts +13 -0
- package/src/health.ts +14 -213
- package/src/healthScore.ts +223 -0
- package/src/icp.ts +19 -35
- package/src/index.ts +28 -1
- package/src/judge.ts +7 -0
- package/src/llm.ts +239 -0
- package/src/market.ts +157 -44
- package/src/marketClassify.ts +26 -2
- package/src/marketTaxonomy.ts +10 -2
- package/src/mcp-bin.ts +34 -2
- package/src/mcp.ts +270 -63
- package/src/progress.ts +197 -0
- package/src/runReport.ts +159 -4
- package/src/schedule.ts +310 -3
- package/src/spoolFiles.ts +116 -0
- package/src/types.ts +32 -2
- package/docs/dx-punch-list.md +0 -87
- package/docs/roadmap-to-1.0.md +0 -211
package/dist/mcp.js
CHANGED
|
@@ -42,6 +42,8 @@ const { McpServer } = await importPeer("@modelcontextprotocol/sdk/server/mcp.js"
|
|
|
42
42
|
const { StdioServerTransport } = await importPeer("@modelcontextprotocol/sdk/server/stdio.js");
|
|
43
43
|
const { z } = await importPeer("zod/v4");
|
|
44
44
|
import { auditSnapshot, defaultPolicy } from "./audit.js";
|
|
45
|
+
import { assertCanonicalSnapshot } from "./cli/shared.js";
|
|
46
|
+
import { nearest } from "./cli/suggest.js";
|
|
45
47
|
import { loadConfig, mergePolicy, resolveConfiguredRules } from "./config.js";
|
|
46
48
|
import { applyPatchPlan } from "./connector.js";
|
|
47
49
|
import { createHubspotConnector } from "./connectors/hubspot.js";
|
|
@@ -50,6 +52,8 @@ import { createStripeConnector } from "./connectors/stripe.js";
|
|
|
50
52
|
import { getCredential, resolveHubspotAccessToken, resolveSalesforceConnection, } from "./credentials.js";
|
|
51
53
|
import { generateDemoSnapshot } from "./demo.js";
|
|
52
54
|
import { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
55
|
+
import { verifyApprovalDigests } from "./integrity.js";
|
|
56
|
+
import { createFilePlanStore } from "./planStore.js";
|
|
53
57
|
import { builtinAuditRules } from "./rules.js";
|
|
54
58
|
import { sampleSnapshot } from "./sampleData.js";
|
|
55
59
|
import { normalizeTranscript, parseCall } from "./calls.js";
|
|
@@ -101,7 +105,9 @@ async function connectorFor(provider) {
|
|
|
101
105
|
}
|
|
102
106
|
return createStripeConnector({ getApiKey: () => key });
|
|
103
107
|
}
|
|
104
|
-
|
|
108
|
+
const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
|
|
109
|
+
throw new Error(`Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
|
|
110
|
+
"Supported providers: hubspot, salesforce, stripe");
|
|
105
111
|
}
|
|
106
112
|
async function readSnapshot(provider, inputPath) {
|
|
107
113
|
if (provider === "demo")
|
|
@@ -112,7 +118,7 @@ async function readSnapshot(provider, inputPath) {
|
|
|
112
118
|
}
|
|
113
119
|
if (!inputPath)
|
|
114
120
|
return sampleSnapshot;
|
|
115
|
-
return JSON.parse(readFileSync(resolve(process.cwd(), inputPath), "utf8"));
|
|
121
|
+
return assertCanonicalSnapshot(JSON.parse(readFileSync(resolve(process.cwd(), inputPath), "utf8")), inputPath);
|
|
116
122
|
}
|
|
117
123
|
function packageVersion() {
|
|
118
124
|
try {
|
|
@@ -123,182 +129,382 @@ function packageVersion() {
|
|
|
123
129
|
return "0.0.0";
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
// Single registration table. startMcpServer() registers exactly this list,
|
|
133
|
+
// and fullstackgtm_capabilities reports its names from the same array — the
|
|
134
|
+
// advertised inventory cannot drift from what is actually registered.
|
|
135
|
+
// (A hand-written parallel tool list was a core defect of the first pass at
|
|
136
|
+
// a capabilities tool, which reported 4 of the 8 registered tools.)
|
|
137
|
+
const toolDefinitions = [
|
|
138
|
+
{
|
|
139
|
+
name: "fullstackgtm_audit",
|
|
140
|
+
writesCrm: false,
|
|
141
|
+
config: {
|
|
142
|
+
title: "GTM Ops Audit",
|
|
143
|
+
description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
|
|
144
|
+
"Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
|
|
145
|
+
"the minimal sample dataset, a snapshot file, or a live provider.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
148
|
+
inputPath: z.string().optional(),
|
|
149
|
+
configPath: z.string().optional(),
|
|
150
|
+
rules: z.array(z.string()).optional(),
|
|
151
|
+
output: z.enum(["json", "markdown"]).optional(),
|
|
152
|
+
today: z.string().optional(),
|
|
153
|
+
staleDealDays: z.number().int().positive().optional(),
|
|
154
|
+
},
|
|
144
155
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
server.registerTool("fullstackgtm_suggest", {
|
|
160
|
-
title: "Suggest Placeholder Values",
|
|
161
|
-
description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
|
|
162
|
-
"evidence (account-name matching, contact associations), with confidence levels and " +
|
|
163
|
-
"reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
|
|
164
|
-
inputSchema: {
|
|
165
|
-
planPath: z.string(),
|
|
166
|
-
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
167
|
-
inputPath: z.string().optional(),
|
|
156
|
+
handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
|
|
157
|
+
const loaded = configPath ? loadConfig(configPath) : null;
|
|
158
|
+
const policy = mergePolicy(defaultPolicy(today), loaded?.config);
|
|
159
|
+
if (today)
|
|
160
|
+
policy.today = today;
|
|
161
|
+
if (staleDealDays !== undefined)
|
|
162
|
+
policy.staleDealDays = staleDealDays;
|
|
163
|
+
const ruleSet = await resolveConfiguredRules(loaded);
|
|
164
|
+
const selected = rules?.length
|
|
165
|
+
? ruleSet.filter((rule) => rules.includes(rule.id))
|
|
166
|
+
: ruleSet;
|
|
167
|
+
const plan = auditSnapshot(await readSnapshot(provider, inputPath), policy, selected);
|
|
168
|
+
return content(output === "markdown" ? patchPlanToMarkdown(plan) : plan);
|
|
168
169
|
},
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
transcriptPath: z.string().optional(),
|
|
184
|
-
title: z.string().optional(),
|
|
185
|
-
source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
|
|
186
|
-
extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
|
|
187
|
-
model: z.string().optional(),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "fullstackgtm_suggest",
|
|
173
|
+
writesCrm: false,
|
|
174
|
+
config: {
|
|
175
|
+
title: "Suggest Placeholder Values",
|
|
176
|
+
description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
|
|
177
|
+
"evidence (account-name matching, contact associations), with confidence levels and " +
|
|
178
|
+
"reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
planPath: z.string(),
|
|
181
|
+
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
182
|
+
inputPath: z.string().optional(),
|
|
183
|
+
},
|
|
188
184
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw new Error("Provide transcript (text) or transcriptPath (file).");
|
|
194
|
-
const mode = extractor ?? "auto";
|
|
195
|
-
const credential = mode === "deterministic" ? null : resolveLlmCredential();
|
|
196
|
-
if (mode === "llm" && !credential) {
|
|
197
|
-
throw new Error("extractor 'llm' needs an API key: set ANTHROPIC_API_KEY or OPENAI_API_KEY in the MCP server environment, or store one with `fullstackgtm login anthropic|openai`.");
|
|
198
|
-
}
|
|
199
|
-
if (credential) {
|
|
200
|
-
const normalized = normalizeTranscript(raw);
|
|
201
|
-
const { insights, model: used } = await extractInsightsLlm(normalized, {
|
|
202
|
-
...credential,
|
|
203
|
-
model,
|
|
204
|
-
title,
|
|
205
|
-
});
|
|
206
|
-
return content(parseCall(raw, { title, sourceSystem: source, insights, extractor: `llm:${credential.provider}:${used}` }));
|
|
207
|
-
}
|
|
208
|
-
return content(parseCall(raw, { title, sourceSystem: source }));
|
|
209
|
-
});
|
|
210
|
-
server.registerTool("fullstackgtm_resolve", {
|
|
211
|
-
title: "Resolve Record (create gate)",
|
|
212
|
-
description: "Before creating a CRM record, check whether it already exists. Returns a verdict " +
|
|
213
|
-
"(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
|
|
214
|
-
"identity keys as the audit/merge engines (account domain, contact email, open-deal " +
|
|
215
|
-
"key). Read-only. Never create on 'exists' or 'ambiguous'.",
|
|
216
|
-
inputSchema: {
|
|
217
|
-
objectType: z.enum(["account", "contact", "deal"]),
|
|
218
|
-
name: z.string().optional(),
|
|
219
|
-
domain: z.string().optional(),
|
|
220
|
-
email: z.string().optional(),
|
|
221
|
-
accountId: z.string().optional(),
|
|
222
|
-
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
223
|
-
inputPath: z.string().optional(),
|
|
185
|
+
handler: async ({ planPath, provider, inputPath }) => {
|
|
186
|
+
const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
|
|
187
|
+
const snapshot = await readSnapshot(provider, inputPath);
|
|
188
|
+
return content({ suggestions: suggestValues(plan, snapshot) });
|
|
224
189
|
},
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
approvedOperationIds: z.array(z.string()).min(1),
|
|
245
|
-
valueOverrides: z.record(z.string(), z.string()).optional(),
|
|
246
|
-
output: z.enum(["json", "markdown"]).optional(),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "fullstackgtm_call_parse",
|
|
193
|
+
writesCrm: false,
|
|
194
|
+
config: {
|
|
195
|
+
title: "Parse Call Transcript",
|
|
196
|
+
description: "Parse a call transcript (Speaker:/[Speaker]: lines or Granola utterance JSON) into " +
|
|
197
|
+
"canonical segments, insights, and GtmEvidence records. extractor: 'auto' (default) " +
|
|
198
|
+
"uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
|
|
199
|
+
"environment or credential store, else the free deterministic keyword baseline; " +
|
|
200
|
+
"'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
transcript: z.string().optional(),
|
|
203
|
+
transcriptPath: z.string().optional(),
|
|
204
|
+
title: z.string().optional(),
|
|
205
|
+
source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
|
|
206
|
+
extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
|
|
207
|
+
model: z.string().optional(),
|
|
208
|
+
},
|
|
247
209
|
},
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
210
|
+
handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
|
|
211
|
+
const raw = transcript ??
|
|
212
|
+
(transcriptPath ? readFileSync(resolve(process.cwd(), transcriptPath), "utf8") : null);
|
|
213
|
+
if (!raw)
|
|
214
|
+
throw new Error("Provide transcript (text) or transcriptPath (file).");
|
|
215
|
+
const mode = extractor ?? "auto";
|
|
216
|
+
const credential = mode === "deterministic" ? null : resolveLlmCredential();
|
|
217
|
+
if (mode === "llm" && !credential) {
|
|
218
|
+
throw new Error("extractor 'llm' needs an API key: set ANTHROPIC_API_KEY or OPENAI_API_KEY in the MCP server environment, or store one with `fullstackgtm login anthropic|openai`.");
|
|
219
|
+
}
|
|
220
|
+
if (credential) {
|
|
221
|
+
const normalized = normalizeTranscript(raw);
|
|
222
|
+
const { insights, model: used } = await extractInsightsLlm(normalized, {
|
|
223
|
+
...credential,
|
|
224
|
+
model,
|
|
225
|
+
title,
|
|
226
|
+
});
|
|
227
|
+
return content(parseCall(raw, { title, sourceSystem: source, insights, extractor: `llm:${credential.provider}:${used}` }));
|
|
228
|
+
}
|
|
229
|
+
return content(parseCall(raw, { title, sourceSystem: source }));
|
|
268
230
|
},
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "fullstackgtm_resolve",
|
|
234
|
+
writesCrm: false,
|
|
235
|
+
config: {
|
|
236
|
+
title: "Resolve Record (create gate)",
|
|
237
|
+
description: "Before creating a CRM record, check whether it already exists. Returns a verdict " +
|
|
238
|
+
"(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
|
|
239
|
+
"identity keys as the audit/merge engines (account domain, contact email, open-deal " +
|
|
240
|
+
"key). Read-only. Never create on 'exists' or 'ambiguous'.",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
objectType: z.enum(["account", "contact", "deal"]),
|
|
243
|
+
name: z.string().optional(),
|
|
244
|
+
domain: z.string().optional(),
|
|
245
|
+
email: z.string().optional(),
|
|
246
|
+
accountId: z.string().optional(),
|
|
247
|
+
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
248
|
+
inputPath: z.string().optional(),
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
|
|
252
|
+
const snapshot = await readSnapshot(provider, inputPath);
|
|
253
|
+
return content(resolveRecord(snapshot, { objectType, name, domain, email, accountId }));
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "fullstackgtm_rules",
|
|
258
|
+
writesCrm: false,
|
|
259
|
+
config: {
|
|
260
|
+
title: "List Audit Rules",
|
|
261
|
+
description: "List the built-in deterministic audit rules with ids and descriptions.",
|
|
262
|
+
inputSchema: {},
|
|
263
|
+
},
|
|
264
|
+
handler: async () => {
|
|
265
|
+
return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "fullstackgtm_apply",
|
|
270
|
+
writesCrm: true,
|
|
271
|
+
config: {
|
|
272
|
+
title: "Apply Approved Patch Operations",
|
|
273
|
+
description: "Apply explicitly approved operations from a patch plan through a provider " +
|
|
274
|
+
"connector. Operations not listed in approvedOperationIds are never written, " +
|
|
275
|
+
"and requires_human_* placeholders need a value override. When the plan is in " +
|
|
276
|
+
"the local plan store (saved via `audit --save`), approvals are verified against " +
|
|
277
|
+
"the store's HMAC approval digests — ids not approved with `plans approve` are " +
|
|
278
|
+
"refused — and the run is recorded onto the stored plan so `plans show` and " +
|
|
279
|
+
"`audit-log export` include it.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
provider: z.enum(["hubspot", "salesforce"]),
|
|
282
|
+
planPath: z.string(),
|
|
283
|
+
approvedOperationIds: z.array(z.string()).min(1),
|
|
284
|
+
valueOverrides: z.record(z.string(), z.string()).optional(),
|
|
285
|
+
output: z.enum(["json", "markdown"]).optional(),
|
|
286
|
+
},
|
|
282
287
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
|
|
289
|
+
// The file may be a raw PatchPlan (audit --out) or a StoredPlan envelope
|
|
290
|
+
// (a file straight out of the plan-store directory).
|
|
291
|
+
const parsed = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
|
|
292
|
+
const filePlan = isStoredPlanFile(parsed) ? parsed.plan : parsed;
|
|
293
|
+
if (!filePlan || !Array.isArray(filePlan.operations)) {
|
|
294
|
+
throw new Error(`${planPath} is not a patch plan (expected { id, operations: [...] }).`);
|
|
295
|
+
}
|
|
296
|
+
// Governance parity with CLI apply: when this plan id exists in the local
|
|
297
|
+
// plan store, the STORE is the source of truth — the approved-id set must
|
|
298
|
+
// come from `plans approve` (which HMAC-signed each approved op), the
|
|
299
|
+
// signatures are re-verified here, and the run is recorded back onto the
|
|
300
|
+
// plan so audit-log export sees MCP applies exactly like CLI applies.
|
|
301
|
+
const store = createFilePlanStore();
|
|
302
|
+
const stored = typeof filePlan.id === "string" && /^[\w.-]+$/.test(filePlan.id)
|
|
303
|
+
? await store.get(filePlan.id)
|
|
304
|
+
: null;
|
|
305
|
+
let plan;
|
|
306
|
+
let effectiveOverrides;
|
|
307
|
+
if (stored) {
|
|
308
|
+
if (stored.status !== "approved") {
|
|
309
|
+
throw new Error(`Plan ${filePlan.id} is ${stored.status}; approve operations first with ` +
|
|
310
|
+
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids|all>\`.`);
|
|
311
|
+
}
|
|
312
|
+
// Downgrade guard (same as CLI apply): an approved plan with no
|
|
313
|
+
// signatures either predates 0.26 or had approvalDigests stripped to
|
|
314
|
+
// skip the integrity check. Refuse rather than trust the file.
|
|
315
|
+
if (stored.approvedOperationIds.length > 0 && !stored.approvalDigests) {
|
|
316
|
+
throw new Error(`Refusing to apply plan ${filePlan.id}: it was approved without integrity signatures ` +
|
|
317
|
+
"(approved before 0.26.0, or its signatures were removed). Re-approve it with " +
|
|
318
|
+
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids|all>\`.`);
|
|
319
|
+
}
|
|
320
|
+
// Never widen: every id the tool wants applied must already be
|
|
321
|
+
// store-approved. (A subset is fine — narrowing never writes more.)
|
|
322
|
+
const storeApproved = new Set(stored.approvedOperationIds);
|
|
323
|
+
const unapproved = approvedOperationIds.filter((id) => !storeApproved.has(id));
|
|
324
|
+
if (unapproved.length > 0) {
|
|
325
|
+
throw new Error(`Refusing to apply plan ${filePlan.id}: operation(s) ${unapproved.join(", ")} were never ` +
|
|
326
|
+
"approved in the plan store. Approve them first with " +
|
|
327
|
+
`\`fullstackgtm plans approve ${filePlan.id} --operations <ids>\`.`);
|
|
328
|
+
}
|
|
329
|
+
// Integrity gate: verify against the EFFECTIVE overrides (stored ∪ tool
|
|
330
|
+
// args), so a tool-supplied value that changes what the human approved
|
|
331
|
+
// is treated as tamper, not a live override — what gets written must
|
|
332
|
+
// equal what was signed.
|
|
333
|
+
effectiveOverrides = { ...stored.valueOverrides, ...(valueOverrides ?? {}) };
|
|
334
|
+
const verification = verifyApprovalDigests(stored.plan.operations, stored.approvedOperationIds, effectiveOverrides, stored.approvalDigests);
|
|
335
|
+
if (!verification.ok) {
|
|
336
|
+
const detail = verification.reason === "no_key"
|
|
337
|
+
? "the plan-signing key is missing (was this plan approved on another machine?). Re-approve it here with `fullstackgtm plans approve`."
|
|
338
|
+
: `these operations differ from what was approved: ${verification.tampered.join(", ")}. ` +
|
|
339
|
+
"If you want a different value, set it at approval (`plans approve --value <op>=<v>`) and re-approve; " +
|
|
340
|
+
"otherwise the plan was edited after approval — review and re-approve.";
|
|
341
|
+
throw new Error(`Refusing to apply plan ${filePlan.id}: ${detail}`);
|
|
342
|
+
}
|
|
343
|
+
plan = stored.plan; // apply what was signed, not what the file says now
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// External plan file, not in the store: no digests exist to verify, but
|
|
347
|
+
// approvals must still reference real operations in the plan content.
|
|
348
|
+
plan = filePlan;
|
|
349
|
+
const known = new Set(plan.operations.map((operation) => operation.id));
|
|
350
|
+
const unknown = approvedOperationIds.filter((id) => !known.has(id));
|
|
351
|
+
if (unknown.length > 0) {
|
|
352
|
+
throw new Error(`Plan ${planPath} has no operation(s) ${unknown.join(", ")} — approvedOperationIds ` +
|
|
353
|
+
"must reference operations in the plan.");
|
|
354
|
+
}
|
|
355
|
+
effectiveOverrides = valueOverrides ?? {};
|
|
356
|
+
}
|
|
357
|
+
const run = await applyPatchPlan(await connectorFor(provider), plan, {
|
|
358
|
+
approvedOperationIds,
|
|
359
|
+
valueOverrides: effectiveOverrides,
|
|
293
360
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
361
|
+
// Persist the run (same data the CLI records) so runs[] and plan status
|
|
362
|
+
// update and audit-log export includes MCP applies. External plan files
|
|
363
|
+
// have no store entry to update — say so instead of silently dropping it.
|
|
364
|
+
let runRecorded = false;
|
|
365
|
+
if (stored) {
|
|
366
|
+
await store.recordRun(plan.id, run);
|
|
367
|
+
runRecorded = true;
|
|
368
|
+
}
|
|
369
|
+
const governance = {
|
|
370
|
+
planSource: stored ? "store" : "external",
|
|
371
|
+
approvalDigestsVerified: stored !== null,
|
|
372
|
+
runRecorded,
|
|
373
|
+
...(stored
|
|
374
|
+
? {}
|
|
375
|
+
: {
|
|
376
|
+
note: "Plan file is not in the local plan store: approvals came from tool arguments " +
|
|
377
|
+
"(verified against the plan content only — no approval-digest check) and the run " +
|
|
378
|
+
"was not recorded. Use `fullstackgtm audit --save` + `plans approve` for " +
|
|
379
|
+
"store-backed governance.",
|
|
380
|
+
}),
|
|
381
|
+
};
|
|
382
|
+
if (output === "markdown") {
|
|
383
|
+
return content(`${formatPatchPlanRun(run)}\n\nGovernance: plan source ${governance.planSource}; ` +
|
|
384
|
+
(runRecorded
|
|
385
|
+
? "approval digests verified; run recorded on the stored plan (audit-log export includes it)."
|
|
386
|
+
: governance.note));
|
|
387
|
+
}
|
|
388
|
+
// Backward compatible: the run's own fields stay top-level; governance is additive.
|
|
389
|
+
return content({ ...run, governance });
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "fullstackgtm_market_worksheet",
|
|
394
|
+
writesCrm: false,
|
|
395
|
+
config: {
|
|
396
|
+
title: "Market Map Classification Worksheet",
|
|
397
|
+
description: "Get everything needed to classify ONE vendor's messaging intensity for a market map: " +
|
|
398
|
+
"the claim taxonomy with judging definitions, the surface rule, and the captured page " +
|
|
399
|
+
"texts. Read each claim's definition, judge loud/quiet/absent from the page texts only, " +
|
|
400
|
+
"and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
|
|
401
|
+
"ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
|
|
402
|
+
"character against the captures, so never paraphrase.",
|
|
403
|
+
inputSchema: {
|
|
404
|
+
vendorId: z.string(),
|
|
405
|
+
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
406
|
+
captureRun: z.string().optional(),
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
handler: async ({ vendorId, configPath, captureRun }) => {
|
|
410
|
+
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
411
|
+
return content(buildWorksheet(config, vendorId, { captureRun }));
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "fullstackgtm_market_observe",
|
|
416
|
+
writesCrm: false,
|
|
417
|
+
config: {
|
|
418
|
+
title: "Submit Market Map Observations",
|
|
419
|
+
description: "Submit a complete ObservationSet (every vendor × claim cell) for a market map run. " +
|
|
420
|
+
"Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
|
|
421
|
+
"span against the stored capture it cites. Returns problems if rejected; nothing is " +
|
|
422
|
+
"stored unless the whole set passes. Observations are append-only — use a new runLabel.",
|
|
423
|
+
inputSchema: {
|
|
424
|
+
observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
|
|
425
|
+
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
handler: async ({ observationsPath, configPath }) => {
|
|
429
|
+
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
430
|
+
const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8"));
|
|
431
|
+
const problems = validateObservationSet(config, set);
|
|
432
|
+
const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
|
|
433
|
+
if (problems.length > 0 || failures.length > 0) {
|
|
434
|
+
return content({
|
|
435
|
+
accepted: false,
|
|
436
|
+
problems,
|
|
437
|
+
spanFailures: failures.map((failure) => `${failure.vendorId} × ${failure.claimId}: ${failure.problem}`),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
await createFileObservationStore(config.category).append(set);
|
|
441
|
+
const fronts = computeFrontStates(config, set);
|
|
442
|
+
return content({ accepted: true, runLabel: set.runLabel, observations: set.observations.length, fronts });
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
// Registered first so agents that list tools see the contract entry up top.
|
|
447
|
+
// Its payload closes over the registration table, so the reported inventory
|
|
448
|
+
// is derived, not hand-maintained.
|
|
449
|
+
toolDefinitions.unshift({
|
|
450
|
+
name: "fullstackgtm_capabilities",
|
|
451
|
+
writesCrm: false,
|
|
452
|
+
config: {
|
|
453
|
+
title: "FullStackGTM Capabilities",
|
|
454
|
+
description: "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
|
|
455
|
+
"server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
|
|
456
|
+
"the CLI entrypoints for everything the tools don't cover.",
|
|
457
|
+
},
|
|
458
|
+
handler: async () => content({
|
|
459
|
+
ok: true,
|
|
460
|
+
tool: "fullstackgtm",
|
|
461
|
+
version: packageVersion(),
|
|
462
|
+
planFirst: true,
|
|
463
|
+
mcpTools: toolDefinitions.map(({ name, writesCrm, config }) => ({
|
|
464
|
+
name,
|
|
465
|
+
title: config.title,
|
|
466
|
+
writesCrm,
|
|
467
|
+
})),
|
|
468
|
+
safety: "Only tools flagged writesCrm can reach a CRM; fullstackgtm_apply writes only operations " +
|
|
469
|
+
"listed in approvedOperationIds and never writes requires_human_* placeholders without a value override.",
|
|
470
|
+
server: { command: "npx -y fullstackgtm-mcp" },
|
|
471
|
+
cli: {
|
|
472
|
+
command: "npx fullstackgtm <command> [--json]",
|
|
473
|
+
capabilities: "npx fullstackgtm capabilities --json",
|
|
474
|
+
agentGuide: "npx fullstackgtm robot-docs",
|
|
475
|
+
},
|
|
476
|
+
examples: [
|
|
477
|
+
"npx -y fullstackgtm-mcp",
|
|
478
|
+
"npx fullstackgtm capabilities --json",
|
|
479
|
+
"npx fullstackgtm audit --demo --json",
|
|
480
|
+
],
|
|
481
|
+
}),
|
|
482
|
+
});
|
|
483
|
+
export async function startMcpServer() {
|
|
484
|
+
const server = new McpServer({
|
|
485
|
+
name: "fullstackgtm",
|
|
486
|
+
version: packageVersion(),
|
|
298
487
|
});
|
|
488
|
+
for (const tool of toolDefinitions) {
|
|
489
|
+
// The table is loosely typed so it can drive one loop; the SDK still
|
|
490
|
+
// validates every call against the zod inputSchema at runtime.
|
|
491
|
+
server.registerTool(tool.name, tool.config, tool.handler);
|
|
492
|
+
}
|
|
299
493
|
const transport = new StdioServerTransport();
|
|
300
494
|
await server.connect(transport);
|
|
301
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* A plan-store file is a StoredPlan envelope ({ plan, status, runs, ... });
|
|
498
|
+
* `audit --out` writes a bare PatchPlan. fullstackgtm_apply accepts either.
|
|
499
|
+
*/
|
|
500
|
+
function isStoredPlanFile(value) {
|
|
501
|
+
if (typeof value !== "object" || value === null || !("plan" in value))
|
|
502
|
+
return false;
|
|
503
|
+
const plan = value.plan;
|
|
504
|
+
return (typeof plan === "object" &&
|
|
505
|
+
plan !== null &&
|
|
506
|
+
Array.isArray(plan.operations));
|
|
507
|
+
}
|
|
302
508
|
function loadMarketConfigOrHint(path) {
|
|
303
509
|
try {
|
|
304
510
|
return loadMarketConfig(path);
|