@loopops/mcp-server 2.4.1 → 2.6.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/index.js CHANGED
@@ -8,6 +8,7 @@ import { registerConfigTools } from "./tools/config.js";
8
8
  import { registerEngTools } from "./tools/eng.js";
9
9
  import { registerCrmTools } from "./tools/crm.js";
10
10
  import { registerEngageTools } from "./tools/engage.js";
11
+ import { registerCompanyGraphTools } from "./tools/company-graph.js";
11
12
  const skillsResponseSchema = z.object({
12
13
  role: z.string(),
13
14
  skills: z.array(z.string()),
@@ -40,5 +41,6 @@ registerConfigTools(server, allowedSkills);
40
41
  registerEngTools(server, allowedSkills);
41
42
  registerCrmTools(server, allowedSkills);
42
43
  registerEngageTools(server, allowedSkills);
44
+ registerCompanyGraphTools(server, allowedSkills);
43
45
  const transport = new StdioServerTransport();
44
46
  await server.connect(transport);
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { z } from "zod";
18
18
  export declare const rangeSchema: z.ZodEnum<["1h", "6h", "24h", "7d", "30d", "90d", "180d", "1y"]>;
19
- export declare const loopNameSchema: z.ZodEnum<["generate", "route", "engage", "pursue", "govern", "plan", "deploy"]>;
19
+ export declare const loopNameSchema: z.ZodEnum<["generate", "qualify", "route", "engage", "pursue", "onboard", "expand", "coach", "design", "deploy"]>;
20
20
  export declare const leadStatusSchema: z.ZodEnum<["Open - Not Contacted", "Working - Contacted", "Qualified", "Closed - Converted", "Closed - Not Converted"]>;
21
21
  export declare const opportunityStageSchema: z.ZodEnum<["Prospecting", "Qualification", "Needs Analysis", "Value Proposition", "Id. Decision Makers", "Perception Analysis", "Proposal/Price Quote", "Negotiation/Review", "Closed Won", "Closed Lost"]>;
22
22
  export declare const pipelineSortSchema: z.ZodEnum<["close_date", "amount", "stage"]>;
@@ -26,7 +26,7 @@ export const rangeSchema = z.enum([
26
26
  "180d",
27
27
  "1y",
28
28
  ]);
29
- export const loopNameSchema = z.enum(["generate", "route", "engage", "pursue", "govern", "plan", "deploy"]);
29
+ export const loopNameSchema = z.enum(["generate", "qualify", "route", "engage", "pursue", "onboard", "expand", "coach", "design", "deploy"]);
30
30
  export const leadStatusSchema = z.enum([
31
31
  "Open - Not Contacted",
32
32
  "Working - Contacted",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Company Graph — MCP tool wrappers (ops+).
3
+ *
4
+ * Read-only tools (Phase 1.4):
5
+ * account_lookup — find accounts by domain / name / external IDs
6
+ * account_show — full detail for one account
7
+ * list_pending_account_matches — review queue for inbound candidates
8
+ *
9
+ * Mutating tools:
10
+ * import_accounts_csv — parse a CSV upload, write inbound candidates,
11
+ * optionally drain via the orchestrator
12
+ *
13
+ * See packages/api/src/routers/mcp.ts for the tRPC procedure
14
+ * implementations and packages/api/src/routers/mcp-schemas.ts for the
15
+ * input schemas.
16
+ */
17
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ export declare function registerCompanyGraphTools(server: McpServer, allowed: Set<string>): void;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Company Graph — MCP tool wrappers (ops+).
3
+ *
4
+ * Read-only tools (Phase 1.4):
5
+ * account_lookup — find accounts by domain / name / external IDs
6
+ * account_show — full detail for one account
7
+ * list_pending_account_matches — review queue for inbound candidates
8
+ *
9
+ * Mutating tools:
10
+ * import_accounts_csv — parse a CSV upload, write inbound candidates,
11
+ * optionally drain via the orchestrator
12
+ *
13
+ * See packages/api/src/routers/mcp.ts for the tRPC procedure
14
+ * implementations and packages/api/src/routers/mcp-schemas.ts for the
15
+ * input schemas.
16
+ */
17
+ import { z } from "zod";
18
+ import { trpcMutation, trpcQuery } from "../api-client.js";
19
+ import { safeTool } from "./_helpers.js";
20
+ export function registerCompanyGraphTools(server, allowed) {
21
+ if (allowed.has("account_lookup")) {
22
+ server.tool("account_lookup", "Find Company Graph accounts by domain, name, account_id, SF Account ID, or legal-entity identifier (DUNS/LEI/EIN). Provide at least one. Multiple keys combine as OR — returns the union. Use this to investigate review-queue candidates or to cross-reference an SF Account back to its CG record.", {
23
+ domain: z
24
+ .string()
25
+ .min(1)
26
+ .optional()
27
+ .describe("Domain to look up (e.g. 'acme.com'). Email addresses also accepted; the matcher extracts the domain."),
28
+ accountName: z
29
+ .string()
30
+ .min(1)
31
+ .optional()
32
+ .describe("Company name to match. Looks up by exact-after-normalization (Inc/LLC/etc. stripped, punctuation collapsed)."),
33
+ accountId: z
34
+ .string()
35
+ .uuid()
36
+ .optional()
37
+ .describe("Company Graph account_id (UUID)."),
38
+ sfAccountId: z
39
+ .string()
40
+ .min(3)
41
+ .optional()
42
+ .describe("Salesforce Account record ID (15- or 18-char SF id)."),
43
+ duns: z.string().min(1).optional().describe("D&B DUNS identifier."),
44
+ lei: z.string().min(1).optional().describe("Legal Entity Identifier (LEI)."),
45
+ ein: z.string().min(1).optional().describe("US EIN."),
46
+ }, safeTool(async (input) => trpcQuery("mcp.accountLookup", input)));
47
+ }
48
+ if (allowed.has("account_show")) {
49
+ server.tool("account_show", "Show full detail for one Company Graph account: identifiers, lifecycle states (enrichment / scoring / deployment), linked legal entities, resolved attributes (the golden record with provenance), latest ICP score, TAL memberships, Salesforce mapping, and recent lifecycle events. Use account_lookup first if you only have a domain or name.", {
50
+ accountId: z
51
+ .string()
52
+ .uuid()
53
+ .describe("Company Graph account_id (UUID). Use account_lookup if you don't have it yet."),
54
+ }, safeTool(async ({ accountId }) => trpcQuery("mcp.accountShow", { accountId })));
55
+ }
56
+ if (allowed.has("list_pending_account_matches")) {
57
+ server.tool("list_pending_account_matches", "Show inbound candidates currently in `match_status='needs_review'` — the queue of inbound rows the matcher couldn't confidently resolve. Each entry includes the source, ingestion time, and the normalized attributes the matcher saw, so you can run `account_lookup` against them to investigate.", {
58
+ limit: z
59
+ .number()
60
+ .int()
61
+ .positive()
62
+ .max(200)
63
+ .optional()
64
+ .describe("Max candidates to return (1–200). Default: 50."),
65
+ source: z
66
+ .string()
67
+ .min(1)
68
+ .optional()
69
+ .describe("Filter to a single inbound source (e.g. 'csv_import', 'backfill'). Omit to see all."),
70
+ }, safeTool(async (input) => trpcQuery("mcp.listPendingAccountMatches", input)));
71
+ }
72
+ if (allowed.has("import_accounts_csv")) {
73
+ server.tool("import_accounts_csv", [
74
+ "Import a CSV of accounts into the Company Graph. Headers are case- and",
75
+ "punctuation-tolerant — `Company Name`, `name`, `Web Site`, `Billing Country`,",
76
+ "`DUNS`, etc. all map to canonical fields. Rows without a domain or",
77
+ "(account_name + country) are skipped. Each accepted row becomes an",
78
+ "`inbound_account_candidate` with the given source; with `process: true` they",
79
+ "drain through the matcher immediately, otherwise they wait for the next",
80
+ "`cg_match_orchestrator` cron tick. Use `dry_run: true` to preview without",
81
+ "writing — recommended on the first import from any new source.",
82
+ ].join(" "), {
83
+ csv: z
84
+ .string()
85
+ .min(1)
86
+ .describe("CSV content as text. First row must be the header. Accepts the same header aliases as the cg-csv-import.mjs script."),
87
+ source: z
88
+ .string()
89
+ .min(1)
90
+ .max(64)
91
+ .regex(/^[a-z0-9_]+$/)
92
+ .optional()
93
+ .describe("Slug stored in `inbound_account_candidate.source` (snake_case). Defaults to 'csv_import'. Use a more specific value for traceability (e.g. 'partner_acme', 'tradeshow_2026q2')."),
94
+ dryRun: z
95
+ .boolean()
96
+ .optional()
97
+ .describe("If true, parse + normalize + summarize but write nothing. Use to preview unfamiliar CSV shapes."),
98
+ process: z
99
+ .boolean()
100
+ .optional()
101
+ .describe("If true, drain the queue via the matcher immediately after insert."),
102
+ }, safeTool(async (input) => trpcMutation("mcp.importAccountsCsv", input)));
103
+ }
104
+ }
@@ -57,7 +57,7 @@ export function registerConfigTools(server, allowed) {
57
57
  }, safeTool(async ({ slug }) => trpcMutation("mcp.activateLoop", { slug })));
58
58
  }
59
59
  if (allowed.has("sync_territories")) {
60
- server.tool("sync_territories", "Reconcile config/plan/hierarchy.yaml (tree) + config/deploy/assignments.yaml (user coverage) with Salesforce ETM (Territory2 + UserTerritory2Association records). Dry-run by default — returns a diff summary. Pass dryRun:false to actually write to Salesforce. Safe to re-run; idempotent.", {
60
+ server.tool("sync_territories", "Reconcile config/design/hierarchy.yaml (tree) + config/deploy/assignments.yaml (user coverage) with Salesforce ETM (Territory2 + UserTerritory2Association records). Dry-run by default — returns a diff summary. Pass dryRun:false to actually write to Salesforce. Safe to re-run; idempotent.", {
61
61
  dryRun: z
62
62
  .boolean()
63
63
  .default(true)
@@ -69,7 +69,7 @@ export function registerConfigTools(server, allowed) {
69
69
  }, safeTool(async ({ dryRun, branch }) => trpcMutation("mcp.syncTerritories", { dryRun, branch })));
70
70
  }
71
71
  if (allowed.has("list_scenarios")) {
72
- server.tool("list_scenarios", "List every planning scenario declared in config/plan/scenarios/. Each scenario is a complete, self-describing plan (declares its own roster, targets, target_productivity). Shows which scenario is currently active (per capacity_config.yaml) and flags any scenarios that are missing required components. Discovery tool — use this before running capacity_report with a specific scenarioId.", {
72
+ server.tool("list_scenarios", "List every planning scenario declared in config/design/scenarios/. Each scenario is a complete, self-describing plan (declares its own roster, targets, target_productivity). Shows which scenario is currently active (per capacity_config.yaml) and flags any scenarios that are missing required components. Discovery tool — use this before running capacity_report with a specific scenarioId.", {
73
73
  branch: z
74
74
  .string()
75
75
  .optional()
@@ -77,7 +77,7 @@ export function registerConfigTools(server, allowed) {
77
77
  }, safeTool(async ({ branch }) => trpcMutation("mcp.listScenarios", { branch })));
78
78
  }
79
79
  if (allowed.has("capacity_report")) {
80
- server.tool("capacity_report", "Run the Plan capacity model: rolls each AE's ramp-aware contribution into territory totals at the configured target_depth and compares against targets.yaml. Returns one table per measure (capacity, target, gap, % attainment). Pure config-driven math; updating roster.yaml or target_productivity.yaml and re-running shows the new plan immediately. No writes.", {
80
+ server.tool("capacity_report", "Run the Design capacity model: rolls each AE's ramp-aware contribution into territory totals at the configured target_depth and compares against targets.yaml. Returns one table per measure (capacity, target, gap, % attainment). Pure config-driven math; updating roster.yaml or target_productivity.yaml and re-running shows the new plan immediately. No writes.", {
81
81
  scenarioId: z
82
82
  .string()
83
83
  .optional()
@@ -108,10 +108,10 @@ export function registerConfigTools(server, allowed) {
108
108
  })));
109
109
  }
110
110
  if (allowed.has("compare_scenarios")) {
111
- server.tool("compare_scenarios", "Run the Plan capacity model for two scenarios and render a side-by-side comparison per measure. For each (territory × measure) cell, shows A vs B capacity, target, gap, and the delta (B−A). Use when evaluating a stretch / conservative / hiring-delay scenario against `base` before deciding whether to promote. No writes — neither scenario's `active` status changes.", {
111
+ server.tool("compare_scenarios", "Run the Design capacity model for two scenarios and render a side-by-side comparison per measure. For each (territory × measure) cell, shows A vs B capacity, target, gap, and the delta (B−A). Use when evaluating a stretch / conservative / hiring-delay scenario against `base` before deciding whether to promote. No writes — neither scenario's `active` status changes.", {
112
112
  scenarioA: z
113
113
  .string()
114
- .describe("First scenario id (e.g. 'base'). Must exist in config/plan/scenarios/ and be complete."),
114
+ .describe("First scenario id (e.g. 'base'). Must exist in config/design/scenarios/ and be complete."),
115
115
  scenarioB: z
116
116
  .string()
117
117
  .describe("Second scenario id to compare against A (e.g. 'stretch'). Must exist and be complete."),
@@ -137,10 +137,10 @@ export function registerConfigTools(server, allowed) {
137
137
  })));
138
138
  }
139
139
  if (allowed.has("promote_scenario")) {
140
- server.tool("promote_scenario", "Promote a scenario to active by editing `active_scenario` in config/plan/capacity_config.yaml. Fails hard if the scenario doesn't resolve (missing or dangling components) — won't promote a broken plan. Dry-run by default — returns the diff; pass dryRun:false to commit the change.", {
140
+ server.tool("promote_scenario", "Promote a scenario to active by editing `active_scenario` in config/design/capacity_config.yaml. Fails hard if the scenario doesn't resolve (missing or dangling components) — won't promote a broken plan. Dry-run by default — returns the diff; pass dryRun:false to commit the change.", {
141
141
  scenarioId: z
142
142
  .string()
143
- .describe("Scenario id to promote (e.g. 'stretch'). Must exist in config/plan/scenarios/ and be complete."),
143
+ .describe("Scenario id to promote (e.g. 'stretch'). Must exist in config/design/scenarios/ and be complete."),
144
144
  dryRun: z
145
145
  .boolean()
146
146
  .default(true)
@@ -152,10 +152,10 @@ export function registerConfigTools(server, allowed) {
152
152
  }, safeTool(async ({ scenarioId, dryRun, branch }) => trpcMutation("mcp.promoteScenario", { scenarioId, dryRun, branch })));
153
153
  }
154
154
  if (allowed.has("gap_analysis")) {
155
- server.tool("gap_analysis", "Use a Claude agent to find the smallest change set that closes a scenario's capacity gap on the target measure. The agent iterates simulate_capacity calls (mutating roster + target_productivity in memory, calling the existing /capacity endpoint) up to a budget cap, then submits a structured proposal. Writes the converged proposal as a draft scenario at `config/plan/scenarios/proposed-gap-{measure}-{ts}.yaml` (override with `writeFile: false`). Use `compare_scenarios` to verify the proposal, then `promote_scenario` to make it active.", {
155
+ server.tool("gap_analysis", "Use a Claude agent to find the smallest change set that closes a scenario's capacity gap on the target measure. The agent iterates simulate_capacity calls (mutating roster + target_productivity in memory, calling the existing /capacity endpoint) up to a budget cap, then submits a structured proposal. Output is exploratory by default — pass `writeFile: true` to materialize the proposal as a draft scenario at `config/design/scenarios/proposed-gap-{measure}-{ts}.yaml`, then use `compare_scenarios` to verify and `promote_scenario` to make it active.", {
156
156
  scenarioId: z
157
157
  .string()
158
- .describe("Scenario whose capacity gap to close (e.g. 'stretch'). Must exist in config/plan/scenarios/ and be complete."),
158
+ .describe("Scenario whose capacity gap to close (e.g. 'stretch'). Must exist in config/design/scenarios/ and be complete."),
159
159
  targetMeasure: z
160
160
  .string()
161
161
  .default("new_acv")
@@ -184,8 +184,8 @@ export function registerConfigTools(server, allowed) {
184
184
  .describe("Max simulate_capacity calls before the agent must submit. Default: 5. Hard cap: 20."),
185
185
  writeFile: z
186
186
  .boolean()
187
- .default(true)
188
- .describe("When true (default), commit the proposal as a draft scenario. Pass false to inspect inline only."),
187
+ .default(false)
188
+ .describe("When true, commit the proposal as a draft scenario for compare_scenarios + promote_scenario. Default: false (exploratory). Pass true to materialize."),
189
189
  branch: z
190
190
  .string()
191
191
  .optional()
@@ -258,7 +258,7 @@ export function registerConfigTools(server, allowed) {
258
258
  })));
259
259
  }
260
260
  if (allowed.has("opportunity_search")) {
261
- server.tool("opportunity_search", "Use a Claude agent to find the deployment of a fixed budget that maximizes capacity gain on a target measure. Sibling to gap_analysis with opposite objective: where gap_analysis closes a gap to zero, opportunity_search maximizes upside under a budget constraint. Same tool surface (simulate_capacity + submit_final_proposal). Writes the agent's best deployment as a draft scenario at `config/plan/scenarios/proposed-opportunity-{measure}-{ts}.yaml` (override with `writeFile: false`). Use `compare_scenarios` to verify, then `promote_scenario` to make it active.", {
261
+ server.tool("opportunity_search", "Use a Claude agent to find the deployment of a fixed budget that maximizes capacity gain on a target measure. Sibling to gap_analysis with opposite objective: where gap_analysis closes a gap to zero, opportunity_search maximizes upside under a budget constraint. Same tool surface (simulate_capacity + submit_final_proposal). Output is exploratory by default — pass `writeFile: true` to materialize the deployment as a draft scenario at `config/design/scenarios/proposed-opportunity-{measure}-{ts}.yaml`, then use `compare_scenarios` to verify and `promote_scenario` to make it active.", {
262
262
  scenarioId: z
263
263
  .string()
264
264
  .describe("Source scenario id (e.g. 'base'). Must exist and be complete."),
@@ -290,8 +290,8 @@ export function registerConfigTools(server, allowed) {
290
290
  .describe("Max simulate_capacity calls before the agent must submit. Default: 5. Hard cap: 20."),
291
291
  writeFile: z
292
292
  .boolean()
293
- .default(true)
294
- .describe("When true (default), commit the deployment as a draft scenario. Pass false to inspect inline only."),
293
+ .default(false)
294
+ .describe("When true, commit the deployment as a draft scenario for compare_scenarios + promote_scenario. Default: false (exploratory). Pass true to materialize."),
295
295
  branch: z
296
296
  .string()
297
297
  .optional()
@@ -327,7 +327,7 @@ export function registerConfigTools(server, allowed) {
327
327
  }, safeTool(async ({ includeReasoning }) => trpcQuery("mcp.listPendingTerritories", { includeReasoning })));
328
328
  }
329
329
  if (allowed.has("assign_territories")) {
330
- server.tool("assign_territories", "Match each target Account to a territory Patch using config/plan/hierarchy.yaml (tree) + config/deploy/territories.yaml (billing match rules). Writes Account.Territory_Slug__c + ObjectTerritory2Association in SF. Dry-run by default. Uses Claude Haiku + web_search as a fallback when deterministic matching fails. Auto-applies agent matches at confidence ≥ 0.9; flags 0.75-0.9 for review.", {
330
+ server.tool("assign_territories", "Match each target Account to a territory Patch using config/design/hierarchy.yaml (tree) + config/deploy/territories.yaml (billing match rules). Writes Account.Territory_Slug__c + ObjectTerritory2Association in SF. Dry-run by default. Uses Claude Haiku + web_search as a fallback when deterministic matching fails. Auto-applies agent matches at confidence ≥ 0.9; flags 0.75-0.9 for review.", {
331
331
  mode: z
332
332
  .enum(["new", "all"])
333
333
  .default("new")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -20,6 +20,12 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsx src/index.ts",
26
+ "start": "node dist/index.js",
27
+ "prepublishOnly": "pnpm build"
28
+ },
23
29
  "dependencies": {
24
30
  "@modelcontextprotocol/sdk": "^1.12.1",
25
31
  "zod": "^3.24.4"
@@ -28,10 +34,5 @@
28
34
  "@types/node": "^22.15.21",
29
35
  "tsx": "^4.19.4",
30
36
  "typescript": "^5.8.3"
31
- },
32
- "scripts": {
33
- "build": "tsc",
34
- "dev": "tsx src/index.ts",
35
- "start": "node dist/index.js"
36
37
  }
37
- }
38
+ }