@loopops/mcp-server 3.9.1 → 3.12.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
@@ -13,6 +13,7 @@ import { registerEngTools } from "./tools/eng.js";
13
13
  import { registerCrmTools } from "./tools/crm.js";
14
14
  import { registerEngageTools } from "./tools/engage.js";
15
15
  import { registerAccountMasterTools } from "./tools/account-master.js";
16
+ import { registerPeopleMasterTools } from "./tools/people-master.js";
16
17
  import { registerScoringTools } from "./tools/scoring.js";
17
18
  import { registerSfdcSyncTools } from "./tools/sfdc-sync.js";
18
19
  import { registerTalTools } from "./tools/tal.js";
@@ -58,6 +59,7 @@ registerEngTools(server, allowedSkills);
58
59
  registerCrmTools(server, allowedSkills);
59
60
  registerEngageTools(server, allowedSkills);
60
61
  registerAccountMasterTools(server, allowedSkills);
62
+ registerPeopleMasterTools(server, allowedSkills);
61
63
  registerTalTools(server, allowedSkills);
62
64
  registerScoringTools(server, allowedSkills);
63
65
  registerSfdcSyncTools(server, allowedSkills);
package/dist/tools/crm.js CHANGED
@@ -1,49 +1,25 @@
1
1
  import { z } from "zod";
2
- import { trpcQuery, trpcMutation } from "../api-client.js";
2
+ import { trpcMutation } from "../api-client.js";
3
3
  import { safeTool } from "./_helpers.js";
4
- import { leadStatusSchema, opportunityStageSchema, pipelineSortSchema, quarterSchema, rangeSchema, salesforceLeadIdSchema, salesforceOpportunityIdSchema, targetAccountTierSchema, } from "./_schemas.js";
4
+ import { leadStatusSchema, salesforceLeadIdSchema, salesforceOpportunityIdSchema, targetAccountTierSchema, } from "./_schemas.js";
5
5
  export function registerCrmTools(server, allowed) {
6
- if (allowed.has("my_pipeline")) {
7
- server.tool("my_pipeline", "List open Salesforce opportunities (deals with forecasts). Reps see their own, managers see their team's, leadership sees all. For new leads that haven't converted yet, use 'my_leads' instead.", {
8
- stage: opportunityStageSchema
9
- .optional()
10
- .describe("Filter by Salesforce stage. Omit to see all open opportunities."),
11
- sort: pipelineSortSchema
12
- .optional()
13
- .describe("Sort order. Default: close_date ascending."),
14
- }, safeTool(async ({ stage, sort }) => trpcQuery("mcp.myPipeline", { stage, sort })));
15
- }
16
- if (allowed.has("my_leads")) {
17
- server.tool("my_leads", "List Salesforce leads assigned to you (or your team if you're a manager) that were created in the given time window. For open opportunities with forecasts, use 'my_pipeline' instead.", {
18
- status: leadStatusSchema
19
- .optional()
20
- .describe("Filter by Salesforce Lead status. E.g., 'Open - Not Contacted'. Omit for any status."),
21
- range: rangeSchema
22
- .optional()
23
- .describe("Time window for lead creation date. E.g., '7d' = last 7 days, '30d' = default, '90d' = last quarter."),
24
- }, safeTool(async ({ status, range }) => trpcQuery("mcp.myLeads", { status, range })));
25
- }
26
- if (allowed.has("pipeline_health")) {
27
- server.tool("pipeline_health", "Pipeline health summary: deals by stage, win rate, average deal size, pipeline velocity.", {
28
- range: rangeSchema
29
- .optional()
30
- .describe("Time window for closed-deal analysis. Longer ranges (30d, 90d, 180d, 1y) give more stable metrics. Default: 90d."),
31
- }, safeTool(async ({ range }) => trpcQuery("mcp.pipelineHealth", { range })));
32
- }
33
- if (allowed.has("rep_performance")) {
34
- server.tool("rep_performance", "Compare rep performance: open pipeline, activities, lead count, win rate.", {
35
- range: rangeSchema
36
- .optional()
37
- .describe("Time window for closed deals and activity. Default: 30d."),
38
- }, safeTool(async ({ range }) => trpcQuery("mcp.repPerformance", { range })));
39
- }
40
- if (allowed.has("forecast")) {
41
- server.tool("forecast", "Pipeline forecast: weighted pipeline by close date, comparison to target.", {
42
- quarter: quarterSchema
43
- .optional()
44
- .describe("Quarter to forecast. Default: current quarter."),
45
- }, safeTool(async ({ quarter }) => trpcQuery("mcp.forecast", { quarter })));
46
- }
6
+ // my_pipeline retired 2026-05-06 — replaced by ClickHouse parameterized
7
+ // view analytics.my_pipeline. Row policies on salesforce.opportunity
8
+ // enforce per-user scoping (rep: own; manager: team; leadership/ops/eng:
9
+ // all). Query via the Cloud Remote MCP server (mcp.clickhouse.cloud/mcp).
10
+ // See docs/CH-ANALYTICS-VIEWS.md.
11
+ // my_leads retired 2026-05-06 — replaced by ClickHouse view
12
+ // analytics.my_leads. Row policies on salesforce.lead enforce per-user
13
+ // scoping. Filter by created_date in the caller. See docs/CH-ANALYTICS-VIEWS.md.
14
+ // pipeline_health retired 2026-05-06 replaced by ClickHouse views
15
+ // analytics.pipeline_by_stage + analytics.win_rate(range_days). Query via
16
+ // the Cloud Remote MCP server (mcp.clickhouse.cloud/mcp). Row policies on
17
+ // salesforce.opportunity enforce per-user scoping. See docs/CH-ANALYTICS-VIEWS.md.
18
+ // rep_performance retired 2026-05-06 — replaced by ClickHouse view
19
+ // analytics.rep_performance(range_days). See docs/CH-ANALYTICS-VIEWS.md.
20
+ // forecast retired 2026-05-06 replaced by ClickHouse view
21
+ // analytics.forecast(quarter_start, quarter_end). Caller computes the
22
+ // quarter window. See docs/CH-ANALYTICS-VIEWS.md.
47
23
  if (allowed.has("update_opportunity")) {
48
24
  server.tool("update_opportunity", "Update an opportunity's close date, next step, or description. You can identify the deal by Salesforce Opportunity ID or exact opportunity name. Reps can only update their own deals. Requires a reason for the change.", {
49
25
  opportunityId: salesforceOpportunityIdSchema
@@ -0,0 +1,22 @@
1
+ /**
2
+ * People Master — MCP tool wrappers (ops+).
3
+ *
4
+ * Read-only tools (Phase 1.3):
5
+ * person_lookup — find people by email / employee_id /
6
+ * person_id / fullName (+ managerEmail to
7
+ * scope a name lookup)
8
+ * person_show — full detail for one person
9
+ * list_pending_people_matches — review queue for inbound candidates
10
+ * walk_hierarchy — preview a hierarchy walk's membership
11
+ *
12
+ * Mutating tools:
13
+ * import_people_csv — parse a Workday-shaped CSV upload,
14
+ * write inbound candidates
15
+ * sync_people_master_resolution_rules — apply YAML → DB and re-resolve
16
+ *
17
+ * See packages/api/src/routers/mcp.ts for the tRPC procedure
18
+ * implementations and packages/api/src/routers/mcp-schemas.ts for the
19
+ * input schemas.
20
+ */
21
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
+ export declare function registerPeopleMasterTools(server: McpServer, allowed: Set<string>): void;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * People Master — MCP tool wrappers (ops+).
3
+ *
4
+ * Read-only tools (Phase 1.3):
5
+ * person_lookup — find people by email / employee_id /
6
+ * person_id / fullName (+ managerEmail to
7
+ * scope a name lookup)
8
+ * person_show — full detail for one person
9
+ * list_pending_people_matches — review queue for inbound candidates
10
+ * walk_hierarchy — preview a hierarchy walk's membership
11
+ *
12
+ * Mutating tools:
13
+ * import_people_csv — parse a Workday-shaped CSV upload,
14
+ * write inbound candidates
15
+ * sync_people_master_resolution_rules — apply YAML → DB and re-resolve
16
+ *
17
+ * See packages/api/src/routers/mcp.ts for the tRPC procedure
18
+ * implementations and packages/api/src/routers/mcp-schemas.ts for the
19
+ * input schemas.
20
+ */
21
+ import { z } from "zod";
22
+ import { trpcMutation, trpcQuery } from "../api-client.js";
23
+ import { CSV_PATH_DESCRIPTION, resolveCsvInput } from "./_csv-input.js";
24
+ import { safeTool } from "./_helpers.js";
25
+ export function registerPeopleMasterTools(server, allowed) {
26
+ if (allowed.has("person_lookup")) {
27
+ server.tool("person_lookup", "Find People Master persons by email, employee_id, person_id, or fullName. Provide at least one. Multiple keys combine as OR — returns the union. Pair fullName with managerEmail when possible to avoid ambiguous name matches.", {
28
+ email: z
29
+ .string()
30
+ .min(1)
31
+ .optional()
32
+ .describe("Work email. Matches both `person.primary_email` and email aliases in `person_identifier`."),
33
+ employeeId: z
34
+ .string()
35
+ .min(1)
36
+ .optional()
37
+ .describe("Workday employee_id (e.g. 'W00123'). Case-sensitive."),
38
+ personId: z
39
+ .string()
40
+ .uuid()
41
+ .optional()
42
+ .describe("People Master person_id (UUID)."),
43
+ fullName: z
44
+ .string()
45
+ .min(1)
46
+ .optional()
47
+ .describe("Full name. Matched after normalization (case-insensitive, punctuation collapsed; apostrophes/hyphens/accents preserved). Returns multiple matches if name is ambiguous — pair with managerEmail."),
48
+ managerEmail: z
49
+ .string()
50
+ .min(1)
51
+ .optional()
52
+ .describe("Manager's work email. Scopes a fullName lookup to that manager's direct reports — recommended whenever you only have a name."),
53
+ }, safeTool(async (input) => trpcQuery("mcp.personLookup", input)));
54
+ }
55
+ if (allowed.has("person_show")) {
56
+ server.tool("person_show", "Show full detail for one person: identifiers, manager + direct reports, lifecycle states (ingestion / activity_capture), resolved attributes (the golden record with provenance), and recent lifecycle events. Use person_lookup first if you only have an email or name.", {
57
+ personId: z
58
+ .string()
59
+ .uuid()
60
+ .describe("person_id (UUID). Use person_lookup if you don't have it yet."),
61
+ }, safeTool(async ({ personId }) => trpcQuery("mcp.personShow", { personId })));
62
+ }
63
+ if (allowed.has("list_pending_people_matches")) {
64
+ server.tool("list_pending_people_matches", "Show inbound person candidates currently in `match_status='needs_review'` — rows the matcher couldn't confidently resolve. Each entry includes the source, ingestion time, and the candidate's email/employee_id so you can investigate via `person_lookup`.", {
65
+ limit: z
66
+ .number()
67
+ .int()
68
+ .positive()
69
+ .max(200)
70
+ .optional()
71
+ .describe("Max candidates to return (1–200). Default: 50."),
72
+ source: z
73
+ .string()
74
+ .min(1)
75
+ .optional()
76
+ .describe("Filter to a single inbound source (e.g. 'workday_csv'). Omit to see all."),
77
+ }, safeTool(async (input) => trpcQuery("mcp.listPendingPeopleMatches", input)));
78
+ }
79
+ if (allowed.has("walk_hierarchy")) {
80
+ server.tool("walk_hierarchy", [
81
+ "Preview a hierarchy walk's membership. Reads `config/people_master/hierarchy_walks.yaml`",
82
+ "from the repo branch (default `main`), runs the evaluator against the live DB, and",
83
+ "returns the member list plus filter diagnostics (how many people were filtered by each",
84
+ "rule). Pass `walkId` to evaluate a specific walk; omit it to list all defined walks.",
85
+ "Use this to sanity-check a walk before wiring it into Activity Capture, or to see who",
86
+ "would be in scope after editing the YAML on a feature branch (pass `branch:` to point",
87
+ "at it).",
88
+ ].join(" "), {
89
+ walkId: z
90
+ .string()
91
+ .min(1)
92
+ .optional()
93
+ .describe("walk_id from hierarchy_walks.yaml (e.g. 'cro_org'). Omit to list all defined walks."),
94
+ branch: z
95
+ .string()
96
+ .optional()
97
+ .describe("Branch to read YAML from. Default: 'main'."),
98
+ limit: z
99
+ .number()
100
+ .int()
101
+ .positive()
102
+ .max(500)
103
+ .optional()
104
+ .describe("Max members to render in the response. Default: 100. Stats reflect the full walk regardless."),
105
+ }, safeTool(async (input) => trpcQuery("mcp.walkHierarchy", input)));
106
+ }
107
+ if (allowed.has("import_people_csv")) {
108
+ server.tool("import_people_csv", [
109
+ "Import a Workday-shaped CSV of people into the People Master. Headers are case- and",
110
+ "punctuation-tolerant — `Worker ID`, `Employee ID`, `Work Email`, `Job Title`, `Manager",
111
+ "Email`, etc. all map to canonical fields. Rows without `work_email` AND without",
112
+ "(`employee_id` + `full_name`) are skipped. Each accepted row becomes an",
113
+ "`inbound_person_candidate` with the given source; with `process: true` they drain",
114
+ "through the matcher immediately, otherwise they wait for the next",
115
+ "`people_master_match_orchestrator` cron tick. Use `dryRun: true` to preview without",
116
+ "writing — recommended on the first import from any new source.",
117
+ ].join(" "), {
118
+ csv: z
119
+ .string()
120
+ .optional()
121
+ .describe("CSV content as text. First row must be the header. Provide this OR csvPath, not both."),
122
+ csvPath: z.string().optional().describe(CSV_PATH_DESCRIPTION),
123
+ source: z
124
+ .string()
125
+ .min(1)
126
+ .max(64)
127
+ .regex(/^[a-z0-9_]+$/)
128
+ .optional()
129
+ .describe("Slug stored in `inbound_person_candidate.source` (snake_case). Defaults to 'workday_csv'."),
130
+ dryRun: z
131
+ .boolean()
132
+ .optional()
133
+ .describe("If true, parse + normalize + summarize but write nothing."),
134
+ process: z
135
+ .boolean()
136
+ .optional()
137
+ .describe("If true, drain the matcher orchestrator immediately after insert."),
138
+ }, safeTool(async (input) => {
139
+ const csv = await resolveCsvInput(input);
140
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
+ const { csv: _csv, csvPath: _csvPath, ...rest } = input;
142
+ return trpcMutation("mcp.importPeopleCsv", { csv, ...rest });
143
+ }));
144
+ }
145
+ if (allowed.has("sync_people_master_resolution_rules")) {
146
+ server.tool("sync_people_master_resolution_rules", [
147
+ "Apply the committed config/people_master/resolution_rules.yaml to the DB.",
148
+ "Server-side equivalent of `node scripts/people-master-sync-resolution-rules.mjs --resolve`.",
149
+ "Idempotent — re-running with no YAML changes is a no-op. Resolver backfills golden",
150
+ "records by default (pass `resolve: false` to skip).",
151
+ ].join(" "), {
152
+ branch: z
153
+ .string()
154
+ .optional()
155
+ .describe("Branch to read YAML from. Default: main. Useful for testing a feature branch's YAML before merging."),
156
+ resolve: z
157
+ .boolean()
158
+ .optional()
159
+ .describe("Re-run the resolver across affected persons after applying rule changes so golden records update. Default: true."),
160
+ dryRun: z
161
+ .boolean()
162
+ .optional()
163
+ .describe("Read + validate + diff against DB without writing. Useful for previewing."),
164
+ }, safeTool(async (input) => trpcMutation("mcp.syncPeopleMasterResolutionRules", input)));
165
+ }
166
+ }
@@ -1,31 +1,14 @@
1
1
  import { trpcQuery } from "../api-client.js";
2
2
  import { safeTool } from "./_helpers.js";
3
- import { loopNameSchema, rangeSchema, salesforceLeadIdSchema, } from "./_schemas.js";
3
+ import { rangeSchema, salesforceLeadIdSchema, } from "./_schemas.js";
4
4
  export function registerReportingTools(server, allowed) {
5
- if (allowed.has("loop_health")) {
6
- server.tool("loop_health", "Query loop execution health: counts, success rate, latency, errors. Filter by loop name and time range.", {
7
- loop: loopNameSchema
8
- .optional()
9
- .describe("Filter to a specific loop. Omit for all loops."),
10
- range: rangeSchema
11
- .optional()
12
- .describe("Time window. Short ranges (1h, 6h, 24h) are most useful. Default: 24h."),
13
- }, safeTool(async ({ loop, range }) => trpcQuery("mcp.loopHealth", { loop, range })));
14
- }
15
- if (allowed.has("scoring_distribution")) {
16
- server.tool("scoring_distribution", "Analyze lead scoring distribution: score histogram, grade breakdown, average score by factor.", {
17
- range: rangeSchema
18
- .optional()
19
- .describe("Time window. Default: 7d."),
20
- }, safeTool(async ({ range }) => trpcQuery("mcp.scoringDistribution", { range })));
21
- }
22
- if (allowed.has("routing_report")) {
23
- server.tool("routing_report", "Analyze routing decisions: rule match rates, queue distribution, agent fallback percentage.", {
24
- range: rangeSchema
25
- .optional()
26
- .describe("Time window. Default: 7d."),
27
- }, safeTool(async ({ range }) => trpcQuery("mcp.routingReport", { range })));
28
- }
5
+ // loop_health retired 2026-05-06 — replaced by ClickHouse parameterized
6
+ // view analytics.loop_health(range_days). Query via the Cloud Remote MCP
7
+ // server (mcp.clickhouse.cloud/mcp). See docs/CH-ANALYTICS-VIEWS.md.
8
+ // scoring_distribution retired 2026-05-06 — replaced by ClickHouse view
9
+ // analytics.scoring_distribution(range_days). See docs/CH-ANALYTICS-VIEWS.md.
10
+ // routing_report retired 2026-05-06 — replaced by ClickHouse view
11
+ // analytics.routing_report(range_days). See docs/CH-ANALYTICS-VIEWS.md.
29
12
  if (allowed.has("enrichment_coverage")) {
30
13
  server.tool("enrichment_coverage", "Analyze lead enrichment coverage: enrichment rate, top industries, headcount distribution.", {
31
14
  range: rangeSchema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "3.9.1",
3
+ "version": "3.12.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -20,12 +20,6 @@
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
- },
29
23
  "dependencies": {
30
24
  "@modelcontextprotocol/sdk": "^1.12.1",
31
25
  "zod": "^3.24.4"
@@ -34,5 +28,10 @@
34
28
  "@types/node": "^22.15.21",
35
29
  "tsx": "^4.19.4",
36
30
  "typescript": "^5.8.3"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc",
34
+ "dev": "tsx src/index.ts",
35
+ "start": "node dist/index.js"
37
36
  }
38
- }
37
+ }