fullstackgtm 0.43.0 → 0.45.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +378 -1
  2. package/README.md +37 -14
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +104 -4556
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/outboxChannel.d.ts +64 -0
  48. package/dist/connectors/outboxChannel.js +170 -0
  49. package/dist/connectors/prospectSources.d.ts +22 -0
  50. package/dist/connectors/prospectSources.js +43 -0
  51. package/dist/connectors/salesforce.d.ts +3 -1
  52. package/dist/connectors/salesforce.js +12 -5
  53. package/dist/connectors/signalSources.d.ts +105 -0
  54. package/dist/connectors/signalSources.js +264 -0
  55. package/dist/connectors/theirstack.d.ts +84 -0
  56. package/dist/connectors/theirstack.js +125 -0
  57. package/dist/icp.d.ts +52 -4
  58. package/dist/icp.js +112 -35
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +1 -0
  61. package/dist/init.js +3 -0
  62. package/dist/judge.d.ts +2 -0
  63. package/dist/judge.js +6 -0
  64. package/dist/judgeEval.d.ts +7 -0
  65. package/dist/judgeEval.js +8 -1
  66. package/dist/marketClassify.d.ts +2 -0
  67. package/dist/marketClassify.js +7 -1
  68. package/dist/mcp-bin.js +2 -2
  69. package/dist/mcp.js +259 -166
  70. package/dist/runReport.d.ts +26 -0
  71. package/dist/runReport.js +15 -0
  72. package/dist/schedule.d.ts +80 -2
  73. package/dist/schedule.js +272 -5
  74. package/dist/signals.d.ts +54 -0
  75. package/dist/signals.js +64 -0
  76. package/dist/spoolFiles.d.ts +44 -0
  77. package/dist/spoolFiles.js +114 -0
  78. package/dist/tam.d.ts +225 -0
  79. package/dist/tam.js +470 -0
  80. package/dist/types.d.ts +11 -0
  81. package/docs/api.md +91 -11
  82. package/docs/outbox-format.md +92 -0
  83. package/docs/recipes.md +37 -0
  84. package/docs/roadmap-to-1.0.md +31 -1
  85. package/docs/signal-spool-format.md +175 -0
  86. package/docs/tam.md +195 -0
  87. package/llms.txt +83 -11
  88. package/package.json +1 -1
  89. package/skills/fullstackgtm/SKILL.md +4 -3
  90. package/src/audit.ts +27 -1
  91. package/src/cli/audit.ts +447 -0
  92. package/src/cli/auth.ts +549 -0
  93. package/src/cli/call.ts +398 -0
  94. package/src/cli/capabilities.ts +215 -0
  95. package/src/cli/draft.ts +101 -0
  96. package/src/cli/enrich.ts +885 -0
  97. package/src/cli/fix.ts +372 -0
  98. package/src/cli/help.ts +664 -0
  99. package/src/cli/icp.ts +265 -0
  100. package/src/cli/init.ts +65 -0
  101. package/src/cli/market.ts +423 -0
  102. package/src/cli/plans.ts +523 -0
  103. package/src/cli/schedule.ts +526 -0
  104. package/src/cli/shared.ts +375 -0
  105. package/src/cli/signals.ts +434 -0
  106. package/src/cli/suggest.ts +151 -0
  107. package/src/cli/tam.ts +435 -0
  108. package/src/cli/ui.ts +426 -0
  109. package/src/cli.ts +103 -5170
  110. package/src/connector.ts +46 -0
  111. package/src/connectors/hubspot.ts +14 -2
  112. package/src/connectors/outboxChannel.ts +202 -0
  113. package/src/connectors/prospectSources.ts +57 -0
  114. package/src/connectors/salesforce.ts +18 -2
  115. package/src/connectors/signalSources.ts +314 -0
  116. package/src/connectors/theirstack.ts +170 -0
  117. package/src/icp.ts +120 -34
  118. package/src/index.ts +32 -0
  119. package/src/init.ts +3 -0
  120. package/src/judge.ts +7 -0
  121. package/src/judgeEval.ts +8 -1
  122. package/src/marketClassify.ts +8 -1
  123. package/src/mcp-bin.ts +2 -2
  124. package/src/mcp.ts +130 -57
  125. package/src/runReport.ts +39 -0
  126. package/src/schedule.ts +330 -7
  127. package/src/signals.ts +90 -0
  128. package/src/spoolFiles.ts +116 -0
  129. package/src/tam.ts +654 -0
  130. package/src/types.ts +12 -0
package/src/index.ts CHANGED
@@ -107,6 +107,38 @@ export {
107
107
  type ScaffoldFile,
108
108
  type ScaffoldOptions,
109
109
  } from "./init.ts";
110
+ export {
111
+ appendCoverage,
112
+ classifyAccount,
113
+ classifyCoverage,
114
+ computeCoverage,
115
+ coverageCountsFromSnapshot,
116
+ coveredAccounts,
117
+ coverageToText,
118
+ crmCheckableCriteria,
119
+ deriveAcvFromClosedWon,
120
+ deriveBuyersPerAccount,
121
+ estimateTam,
122
+ loadTamModel,
123
+ projectEta,
124
+ readCoverageTimeline,
125
+ saveTamModel,
126
+ tamDir,
127
+ tamReportToMarkdown,
128
+ type AccountTamClass,
129
+ type AcvBasis,
130
+ type DerivedAcv,
131
+ type DerivedBuyers,
132
+ type EstimateTamInput,
133
+ type TamClassified,
134
+ type TamCoverage,
135
+ type TamCoverageCounts,
136
+ type TamCrossCheck,
137
+ type TamEta,
138
+ type TamModel,
139
+ type TamTargeting,
140
+ type TamUniverse,
141
+ } from "./tam.ts";
110
142
  export {
111
143
  apolloPullKeysForAppend,
112
144
  apolloPullKeysForRefresh,
package/src/init.ts CHANGED
@@ -45,6 +45,9 @@ export function starterIcp(): Icp {
45
45
  industries: ["software", "saas"],
46
46
  employeeBands: ["51-200", "201-500", "501-1000"],
47
47
  geos: ["us"],
48
+ // Technographic targeting (the real RevOps signal): companies that USE a
49
+ // CRM/MAP. Drives `tam estimate|accounts --source theirstack`.
50
+ technologies: ["salesforce", "hubspot", "pipedrive"],
48
51
  },
49
52
  persona: {
50
53
  jobLevels: ["vp", "director", "manager"],
package/src/judge.ts CHANGED
@@ -612,6 +612,8 @@ export async function judgeSignals(opts: {
612
612
  promptTemplate?: string;
613
613
  llm?: LlmCallOptions;
614
614
  now?: Date;
615
+ /** Per-account progress (presentation only — a throwing callback never fails the run). */
616
+ onAccount?: (done: number, total: number, domain: string) => void;
615
617
  }): Promise<JudgeDecision[]> {
616
618
  const now = opts.now ?? new Date();
617
619
  const signalsById = new Map(opts.signals.map((s) => [s.id, s]));
@@ -629,6 +631,11 @@ export async function judgeSignals(opts: {
629
631
 
630
632
  const decisions: JudgeDecision[] = [];
631
633
  for (const [domain, signals] of byAccount) {
634
+ try {
635
+ opts.onAccount?.(decisions.length, byAccount.size, domain);
636
+ } catch {
637
+ // progress is presentation-only
638
+ }
632
639
  // Memory + fit stay gated on --with-history (scoring semantics unchanged).
633
640
  const recentlyTouched =
634
641
  opts.withHistory && opts.snapshot ? accountRecentlyTouched(domain, opts.snapshot, now) : false;
package/src/judgeEval.ts CHANGED
@@ -205,7 +205,14 @@ export function defaultJudgeFn(opts: { config?: SignalsConfig; icp?: Icp; now?:
205
205
  // ---------------------------------------------------------------------------
206
206
  // The default golden set (ships inline so `icp eval --golden default` is free)
207
207
 
208
- const GOLD_NOW_ISO = "2026-06-23T12:00:00.000Z";
208
+ /**
209
+ * The default golden set's clock. Every `firstSeen` below is relative to this
210
+ * instant, so grading MUST pin `now: new Date(DEFAULT_GOLDEN_NOW_ISO)` —
211
+ * grading against wall time lets freshness decay rot the "fresh → send" rows,
212
+ * and the gate starts failing on a calendar date instead of a code change.
213
+ */
214
+ export const DEFAULT_GOLDEN_NOW_ISO = "2026-06-23T12:00:00.000Z";
215
+ const GOLD_NOW_ISO = DEFAULT_GOLDEN_NOW_ISO;
209
216
 
210
217
  function goldSignal(over: {
211
218
  accountDomain: string;
@@ -113,6 +113,8 @@ export type ClassifyMarketOptions = {
113
113
  /** Captures directory override (tests); defaults to the profile market home. */
114
114
  capturesDir?: string;
115
115
  now?: () => Date;
116
+ /** Per-vendor progress (presentation only — a throwing callback never fails the run). */
117
+ onVendor?: (done: number, total: number, vendorId: string) => void;
116
118
  };
117
119
 
118
120
  export type ClassifyMarketResult = {
@@ -143,7 +145,12 @@ export async function classifyMarket(
143
145
  const observations: MarketObservation[] = [];
144
146
  const retriedVendorIds: string[] = [];
145
147
 
146
- for (const vendorId of vendorIds) {
148
+ for (const [vendorIndex, vendorId] of vendorIds.entries()) {
149
+ try {
150
+ options.onVendor?.(vendorIndex, vendorIds.length, vendorId);
151
+ } catch {
152
+ // progress is presentation-only
153
+ }
147
154
  const vendor = config.vendors.find((candidate) => candidate.id === vendorId);
148
155
  if (!vendor) throw new Error(`Unknown vendor "${vendorId}"`);
149
156
  const vendorEntries = runEntries.filter((entry) => entry.vendorId === vendorId);
package/src/mcp-bin.ts CHANGED
@@ -9,8 +9,8 @@ const MISSING_PEER_HELP = `The MCP server needs the optional peer dependencies @
9
9
  In a project:
10
10
  npm install fullstackgtm @modelcontextprotocol/sdk zod
11
11
 
12
- Zero-install:
13
- npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp`;
12
+ Zero-install (the fullstackgtm-mcp wrapper package bundles the peers):
13
+ npx -y fullstackgtm-mcp`;
14
14
 
15
15
  function isMissingPeerError(error: unknown): boolean {
16
16
  if (!(error instanceof Error)) return false;
package/src/mcp.ts CHANGED
@@ -38,6 +38,8 @@ const { StdioServerTransport } = await importPeer<typeof import("@modelcontextpr
38
38
  );
39
39
  const { z } = await importPeer<typeof import("zod/v4")>("zod/v4");
40
40
  import { auditSnapshot, defaultPolicy } from "./audit.ts";
41
+ import { assertCanonicalSnapshot } from "./cli/shared.ts";
42
+ import { nearest } from "./cli/suggest.ts";
41
43
  import { loadConfig, mergePolicy, resolveConfiguredRules } from "./config.ts";
42
44
  import { applyPatchPlan } from "./connector.ts";
43
45
  import { createHubspotConnector } from "./connectors/hubspot.ts";
@@ -123,8 +125,10 @@ async function connectorFor(provider: string): Promise<GtmConnector> {
123
125
  }
124
126
  return createStripeConnector({ getApiKey: () => key });
125
127
  }
128
+ const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
126
129
  throw new Error(
127
- `Unknown provider: ${provider}. Supported providers: hubspot, salesforce, stripe`,
130
+ `Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
131
+ "Supported providers: hubspot, salesforce, stripe",
128
132
  );
129
133
  }
130
134
 
@@ -138,9 +142,10 @@ async function readSnapshot(
138
142
  return connector.fetchSnapshot();
139
143
  }
140
144
  if (!inputPath) return sampleSnapshot;
141
- return JSON.parse(
142
- readFileSync(resolve(process.cwd(), inputPath), "utf8"),
143
- ) as CanonicalGtmSnapshot;
145
+ return assertCanonicalSnapshot(
146
+ JSON.parse(readFileSync(resolve(process.cwd(), inputPath), "utf8")),
147
+ inputPath,
148
+ );
144
149
  }
145
150
 
146
151
  function packageVersion() {
@@ -152,15 +157,29 @@ function packageVersion() {
152
157
  }
153
158
  }
154
159
 
155
- export async function startMcpServer() {
156
- const server = new McpServer({
157
- name: "fullstackgtm",
158
- version: packageVersion(),
159
- });
160
+ type ToolDefinition = {
161
+ name: string;
162
+ // Whether the tool can write to a CRM. Everything else reads, or writes
163
+ // only local workspace state (market_observe appends to the local
164
+ // observation store).
165
+ writesCrm: boolean;
166
+ config: { title: string; description: string; inputSchema?: Record<string, unknown> };
167
+ // Args are validated by the SDK against config.inputSchema before the
168
+ // handler runs; the loose typing here is what lets one table drive a
169
+ // registration loop instead of per-call generics.
170
+ handler: (args: any) => Promise<ReturnType<typeof content>> | ReturnType<typeof content>;
171
+ };
160
172
 
161
- server.registerTool(
162
- "fullstackgtm_audit",
163
- {
173
+ // Single registration table. startMcpServer() registers exactly this list,
174
+ // and fullstackgtm_capabilities reports its names from the same array — the
175
+ // advertised inventory cannot drift from what is actually registered.
176
+ // (A hand-written parallel tool list was a core defect of the first pass at
177
+ // a capabilities tool, which reported 4 of the 8 registered tools.)
178
+ const toolDefinitions: ToolDefinition[] = [
179
+ {
180
+ name: "fullstackgtm_audit",
181
+ writesCrm: false,
182
+ config: {
164
183
  title: "GTM Ops Audit",
165
184
  description:
166
185
  "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
@@ -176,23 +195,23 @@ export async function startMcpServer() {
176
195
  staleDealDays: z.number().int().positive().optional(),
177
196
  },
178
197
  },
179
- async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
198
+ handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
180
199
  const loaded = configPath ? loadConfig(configPath) : null;
181
200
  const policy = mergePolicy(defaultPolicy(today), loaded?.config);
182
201
  if (today) policy.today = today;
183
202
  if (staleDealDays !== undefined) policy.staleDealDays = staleDealDays;
184
203
  const ruleSet = await resolveConfiguredRules(loaded);
185
204
  const selected = rules?.length
186
- ? ruleSet.filter((rule) => rules.includes(rule.id))
205
+ ? ruleSet.filter((rule: { id: string }) => rules.includes(rule.id))
187
206
  : ruleSet;
188
207
  const plan = auditSnapshot(await readSnapshot(provider, inputPath), policy, selected);
189
208
  return content(output === "markdown" ? patchPlanToMarkdown(plan) : plan);
190
209
  },
191
- );
192
-
193
- server.registerTool(
194
- "fullstackgtm_suggest",
195
- {
210
+ },
211
+ {
212
+ name: "fullstackgtm_suggest",
213
+ writesCrm: false,
214
+ config: {
196
215
  title: "Suggest Placeholder Values",
197
216
  description:
198
217
  "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
@@ -204,16 +223,16 @@ export async function startMcpServer() {
204
223
  inputPath: z.string().optional(),
205
224
  },
206
225
  },
207
- async ({ planPath, provider, inputPath }) => {
226
+ handler: async ({ planPath, provider, inputPath }) => {
208
227
  const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8")) as PatchPlan;
209
228
  const snapshot = await readSnapshot(provider, inputPath);
210
229
  return content({ suggestions: suggestValues(plan, snapshot) });
211
230
  },
212
- );
213
-
214
- server.registerTool(
215
- "fullstackgtm_call_parse",
216
- {
231
+ },
232
+ {
233
+ name: "fullstackgtm_call_parse",
234
+ writesCrm: false,
235
+ config: {
217
236
  title: "Parse Call Transcript",
218
237
  description:
219
238
  "Parse a call transcript (Speaker:/[Speaker]: lines or Granola utterance JSON) into " +
@@ -230,7 +249,7 @@ export async function startMcpServer() {
230
249
  model: z.string().optional(),
231
250
  },
232
251
  },
233
- async ({ transcript, transcriptPath, title, source, extractor, model }) => {
252
+ handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
234
253
  const raw =
235
254
  transcript ??
236
255
  (transcriptPath ? readFileSync(resolve(process.cwd(), transcriptPath), "utf8") : null);
@@ -255,11 +274,11 @@ export async function startMcpServer() {
255
274
  }
256
275
  return content(parseCall(raw, { title, sourceSystem: source }));
257
276
  },
258
- );
259
-
260
- server.registerTool(
261
- "fullstackgtm_resolve",
262
- {
277
+ },
278
+ {
279
+ name: "fullstackgtm_resolve",
280
+ writesCrm: false,
281
+ config: {
263
282
  title: "Resolve Record (create gate)",
264
283
  description:
265
284
  "Before creating a CRM record, check whether it already exists. Returns a verdict " +
@@ -276,29 +295,29 @@ export async function startMcpServer() {
276
295
  inputPath: z.string().optional(),
277
296
  },
278
297
  },
279
- async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
298
+ handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
280
299
  const snapshot = await readSnapshot(provider, inputPath);
281
300
  return content(resolveRecord(snapshot, { objectType, name, domain, email, accountId }));
282
301
  },
283
- );
284
-
285
- server.registerTool(
286
- "fullstackgtm_rules",
287
- {
302
+ },
303
+ {
304
+ name: "fullstackgtm_rules",
305
+ writesCrm: false,
306
+ config: {
288
307
  title: "List Audit Rules",
289
308
  description: "List the built-in deterministic audit rules with ids and descriptions.",
290
309
  inputSchema: {},
291
310
  },
292
- async () => {
311
+ handler: async () => {
293
312
  return content(
294
313
  builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })),
295
314
  );
296
315
  },
297
- );
298
-
299
- server.registerTool(
300
- "fullstackgtm_apply",
301
- {
316
+ },
317
+ {
318
+ name: "fullstackgtm_apply",
319
+ writesCrm: true,
320
+ config: {
302
321
  title: "Apply Approved Patch Operations",
303
322
  description:
304
323
  "Apply explicitly approved operations from a patch plan through a provider " +
@@ -312,7 +331,7 @@ export async function startMcpServer() {
312
331
  output: z.enum(["json", "markdown"]).optional(),
313
332
  },
314
333
  },
315
- async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
334
+ handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
316
335
  const plan = JSON.parse(
317
336
  readFileSync(resolve(process.cwd(), planPath), "utf8"),
318
337
  ) as PatchPlan;
@@ -322,11 +341,11 @@ export async function startMcpServer() {
322
341
  });
323
342
  return content(output === "markdown" ? formatPatchPlanRun(run) : run);
324
343
  },
325
- );
326
-
327
- server.registerTool(
328
- "fullstackgtm_market_worksheet",
329
- {
344
+ },
345
+ {
346
+ name: "fullstackgtm_market_worksheet",
347
+ writesCrm: false,
348
+ config: {
330
349
  title: "Market Map Classification Worksheet",
331
350
  description:
332
351
  "Get everything needed to classify ONE vendor's messaging intensity for a market map: " +
@@ -341,15 +360,15 @@ export async function startMcpServer() {
341
360
  captureRun: z.string().optional(),
342
361
  },
343
362
  },
344
- async ({ vendorId, configPath, captureRun }) => {
363
+ handler: async ({ vendorId, configPath, captureRun }) => {
345
364
  const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
346
365
  return content(buildWorksheet(config, vendorId, { captureRun }));
347
366
  },
348
- );
349
-
350
- server.registerTool(
351
- "fullstackgtm_market_observe",
352
- {
367
+ },
368
+ {
369
+ name: "fullstackgtm_market_observe",
370
+ writesCrm: false,
371
+ config: {
353
372
  title: "Submit Market Map Observations",
354
373
  description:
355
374
  "Submit a complete ObservationSet (every vendor × claim cell) for a market map run. " +
@@ -361,7 +380,7 @@ export async function startMcpServer() {
361
380
  configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
362
381
  },
363
382
  },
364
- async ({ observationsPath, configPath }) => {
383
+ handler: async ({ observationsPath, configPath }) => {
365
384
  const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
366
385
  const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8")) as ObservationSet;
367
386
  const problems = validateObservationSet(config, set);
@@ -377,7 +396,61 @@ export async function startMcpServer() {
377
396
  const fronts = computeFrontStates(config, set);
378
397
  return content({ accepted: true, runLabel: set.runLabel, observations: set.observations.length, fronts });
379
398
  },
380
- );
399
+ },
400
+ ];
401
+
402
+ // Registered first so agents that list tools see the contract entry up top.
403
+ // Its payload closes over the registration table, so the reported inventory
404
+ // is derived, not hand-maintained.
405
+ toolDefinitions.unshift({
406
+ name: "fullstackgtm_capabilities",
407
+ writesCrm: false,
408
+ config: {
409
+ title: "FullStackGTM Capabilities",
410
+ description:
411
+ "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
412
+ "server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
413
+ "the CLI entrypoints for everything the tools don't cover.",
414
+ },
415
+ handler: async () =>
416
+ content({
417
+ ok: true,
418
+ tool: "fullstackgtm",
419
+ version: packageVersion(),
420
+ planFirst: true,
421
+ mcpTools: toolDefinitions.map(({ name, writesCrm, config }) => ({
422
+ name,
423
+ title: config.title,
424
+ writesCrm,
425
+ })),
426
+ safety:
427
+ "Only tools flagged writesCrm can reach a CRM; fullstackgtm_apply writes only operations " +
428
+ "listed in approvedOperationIds and never writes requires_human_* placeholders without a value override.",
429
+ server: { command: "npx -y fullstackgtm-mcp" },
430
+ cli: {
431
+ command: "npx fullstackgtm <command> [--json]",
432
+ capabilities: "npx fullstackgtm capabilities --json",
433
+ agentGuide: "npx fullstackgtm robot-docs",
434
+ },
435
+ examples: [
436
+ "npx -y fullstackgtm-mcp",
437
+ "npx fullstackgtm capabilities --json",
438
+ "npx fullstackgtm audit --demo --json",
439
+ ],
440
+ }),
441
+ });
442
+
443
+ export async function startMcpServer() {
444
+ const server = new McpServer({
445
+ name: "fullstackgtm",
446
+ version: packageVersion(),
447
+ });
448
+
449
+ for (const tool of toolDefinitions) {
450
+ // The table is loosely typed so it can drive one loop; the SDK still
451
+ // validates every call against the zod inputSchema at runtime.
452
+ server.registerTool(tool.name, tool.config as never, tool.handler as never);
453
+ }
381
454
 
382
455
  const transport = new StdioServerTransport();
383
456
  await server.connect(transport);
package/src/runReport.ts CHANGED
@@ -11,7 +11,34 @@ import { getCredential } from "./credentials.ts";
11
11
 
12
12
  type RunEvent = { ts: number; type: string; detail?: string };
13
13
 
14
+ /**
15
+ * Per-row finding for the hosted run timeline. IDs + issue type ONLY — no field
16
+ * values ever leave the CLI, keeping the audit's row data on the operator's side
17
+ * while still giving the dashboard navigable, filterable detail.
18
+ */
19
+ export type RunFinding = {
20
+ objectType: string;
21
+ objectId: string;
22
+ severity: string;
23
+ ruleId: string;
24
+ field?: string;
25
+ operation?: string;
26
+ };
27
+
28
+ /**
29
+ * Where a run's findings live in the source CRM, so the dashboard can deep-link
30
+ * each record. `recordUrlBase` is the provider's record URL prefix; the
31
+ * dashboard appends the per-object path. Just an URL prefix — no record data.
32
+ */
33
+ export type RunCrm = { provider: string; recordUrlBase: string };
34
+
35
+ // Bound the fire-and-forget POST: a pathological audit could surface thousands
36
+ // of findings; cap what we ship (counts still carry the true total).
37
+ const MAX_REPORTED_FINDINGS = 2000;
38
+
14
39
  let counts: Record<string, unknown> | undefined;
40
+ let findings: RunFinding[] | undefined;
41
+ let crm: RunCrm | undefined;
15
42
  const events: RunEvent[] = [];
16
43
 
17
44
  /** A command annotates its headline metrics (merged). */
@@ -19,6 +46,16 @@ export function reportCounts(values: Record<string, unknown>): void {
19
46
  counts = { ...(counts ?? {}), ...values };
20
47
  }
21
48
 
49
+ /** A command reports per-row findings (IDs + issue type, no values). */
50
+ export function reportFindings(values: RunFinding[]): void {
51
+ findings = values.slice(0, MAX_REPORTED_FINDINGS);
52
+ }
53
+
54
+ /** A command reports the source CRM's record-URL base for dashboard deep-links. */
55
+ export function reportCrm(value: RunCrm): void {
56
+ crm = value;
57
+ }
58
+
22
59
  /** A command annotates a structured event (plan saved, meter charged, …). */
23
60
  export function reportEvent(type: string, detail?: string): void {
24
61
  events.push({ ts: Date.now(), type, detail });
@@ -65,6 +102,8 @@ export async function flushRunReport(
65
102
  finishedAt,
66
103
  durationMs: finishedAt - startedAt,
67
104
  counts,
105
+ findings: findings?.length ? findings : undefined,
106
+ crm,
68
107
  events: events.length ? events : undefined,
69
108
  error,
70
109
  }),