@loopops/mcp-server 3.2.0 → 3.4.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.
@@ -272,10 +272,11 @@ function persistRotatedRefreshToken(newToken) {
272
272
  */
273
273
  async function diagnoseRefreshFailure(status, body) {
274
274
  const head = `Okta refresh failed (HTTP ${status}). Detail: ${body.slice(0, 300)}`;
275
- const generic = "Your refresh token may be revoked or idle-expired. Re-run " +
276
- "`npx @loopops/mcp-cli@latest login` (the `@latest` pin bypasses the " +
277
- "npx cache, which can serve a stale cli with old tenant defaults for " +
278
- "up to 24h after a publish).";
275
+ const generic = "Your refresh token may be revoked or idle-expired.\n" +
276
+ " Diagnose: `npx @loopops/mcp-cli@latest doctor` checks tenant config + token.\n" +
277
+ " Fix: `npx @loopops/mcp-cli@latest login` re-authenticates from scratch.\n" +
278
+ "(The `@latest` pin matters: bare `npx @loopops/mcp-cli` may serve a " +
279
+ "cached older cli with stale tenant defaults for up to 24h after a publish.)";
279
280
  if (!apiUrl)
280
281
  return `${head}\n\n${generic}`;
281
282
  try {
package/dist/index.js CHANGED
@@ -1,14 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
7
  import { z } from "zod";
5
8
  import { trpcQuery } from "./api-client.js";
9
+ import { registerIdentityTools } from "./tools/identity.js";
6
10
  import { registerReportingTools } from "./tools/reporting.js";
7
11
  import { registerConfigTools } from "./tools/config.js";
8
12
  import { registerEngTools } from "./tools/eng.js";
9
13
  import { registerCrmTools } from "./tools/crm.js";
10
14
  import { registerEngageTools } from "./tools/engage.js";
11
15
  import { registerAccountMasterTools } from "./tools/account-master.js";
16
+ import { registerTalTools } from "./tools/tal.js";
17
+ // Read our own package.json at runtime so the version baked into MCP
18
+ // initialize-handshake `serverInfo.version` matches the published npm
19
+ // version. Was hardcoded "1.0.0" — confusing in Claude Desktop logs and
20
+ // any other MCP client that displays server version. `package.json` is
21
+ // always included in npm tarballs, so the relative path `../package.json`
22
+ // (from dist/index.js) resolves cleanly post-publish.
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
12
25
  const skillsResponseSchema = z.object({
13
26
  role: z.string(),
14
27
  skills: z.array(z.string()),
@@ -34,13 +47,15 @@ const allowedSkills = new Set(skills);
34
47
  console.error(`[MCP] Connected — role: ${role}, ${skills.length} skills`);
35
48
  const server = new McpServer({
36
49
  name: "loop-operations",
37
- version: "1.0.0",
50
+ version: pkg.version,
38
51
  });
52
+ registerIdentityTools(server, allowedSkills);
39
53
  registerReportingTools(server, allowedSkills);
40
54
  registerConfigTools(server, allowedSkills);
41
55
  registerEngTools(server, allowedSkills);
42
56
  registerCrmTools(server, allowedSkills);
43
57
  registerEngageTools(server, allowedSkills);
44
58
  registerAccountMasterTools(server, allowedSkills);
59
+ registerTalTools(server, allowedSkills);
45
60
  const transport = new StdioServerTransport();
46
61
  await server.connect(transport);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Identity / connection-check tools.
3
+ *
4
+ * Currently just `mcp_whoami` — a data-independent first-call sanity check.
5
+ * Returns the caller's email, MCP role, SF user link state, and tool count.
6
+ * Designed for the onboarding flow: after install + restart, asking Claude
7
+ * "are you connected?" should resolve to this and confirm everything wired
8
+ * correctly without depending on having any actual Loop data yet.
9
+ */
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ export declare function registerIdentityTools(server: McpServer, allowed: Set<string>): void;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Identity / connection-check tools.
3
+ *
4
+ * Currently just `mcp_whoami` — a data-independent first-call sanity check.
5
+ * Returns the caller's email, MCP role, SF user link state, and tool count.
6
+ * Designed for the onboarding flow: after install + restart, asking Claude
7
+ * "are you connected?" should resolve to this and confirm everything wired
8
+ * correctly without depending on having any actual Loop data yet.
9
+ */
10
+ import { trpcQuery } from "../api-client.js";
11
+ import { safeTool } from "./_helpers.js";
12
+ export function registerIdentityTools(server, allowed) {
13
+ if (allowed.has("mcp_whoami")) {
14
+ server.tool("mcp_whoami", [
15
+ "Connection sanity check. Returns your email, MCP role, Salesforce user link state,",
16
+ "and the number of tools available to you. Use as a first-call verification after",
17
+ "install — works regardless of whether you have any Loop data yet, so it's a clean",
18
+ "way to confirm the auth chain (Okta → Loop → role resolution) is wired correctly.",
19
+ ].join(" "), {}, safeTool(async () => trpcQuery("mcp.whoami")));
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Target Account List (TAL) MCP tool wrappers.
3
+ *
4
+ * Phase 3.5.5 of docs/ACCOUNT-MASTER-PLAN.md.
5
+ *
6
+ * Tools (ops + eng):
7
+ * list_tals — catalog of every TAL with member counts
8
+ * show_tal_membership — paginated members of one TAL with key attributes
9
+ * evaluate_tal_now — manual trigger for the evaluator (one TAL)
10
+ * create_tal — define a new TAL with criteria + cadence
11
+ *
12
+ * tRPC procedures live in packages/api/src/routers/mcp.ts; input schemas
13
+ * in packages/api/src/routers/mcp-schemas.ts.
14
+ */
15
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ export declare function registerTalTools(server: McpServer, allowed: Set<string>): void;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Target Account List (TAL) MCP tool wrappers.
3
+ *
4
+ * Phase 3.5.5 of docs/ACCOUNT-MASTER-PLAN.md.
5
+ *
6
+ * Tools (ops + eng):
7
+ * list_tals — catalog of every TAL with member counts
8
+ * show_tal_membership — paginated members of one TAL with key attributes
9
+ * evaluate_tal_now — manual trigger for the evaluator (one TAL)
10
+ * create_tal — define a new TAL with criteria + cadence
11
+ *
12
+ * tRPC procedures live in packages/api/src/routers/mcp.ts; input schemas
13
+ * in packages/api/src/routers/mcp-schemas.ts.
14
+ */
15
+ import { z } from "zod";
16
+ import { trpcMutation, trpcQuery } from "../api-client.js";
17
+ import { safeTool } from "./_helpers.js";
18
+ // Local replicas of the criteria DSL types — kept narrow so the MCP
19
+ // tool description doesn't have to ship the full zod schema. The
20
+ // authoritative validator lives in mcp-schemas.ts; this is just for
21
+ // tool-input typing on the client side.
22
+ const conditionSchema = z.lazy(() => z.union([
23
+ z.object({
24
+ operator: z.enum(["AND", "OR", "NOT"]),
25
+ conditions: z.array(conditionSchema),
26
+ }),
27
+ z.object({
28
+ type: z.enum([
29
+ "attribute",
30
+ "score",
31
+ "lifecycle_state",
32
+ "member_of",
33
+ "exclusion",
34
+ ]),
35
+ }).passthrough(),
36
+ ]));
37
+ const talIdentifierShape = {
38
+ talName: z
39
+ .string()
40
+ .min(1)
41
+ .optional()
42
+ .describe("Human-readable TAL name (e.g. 'signal_scanner_targets'). Easier to type than the UUID."),
43
+ talId: z
44
+ .string()
45
+ .uuid()
46
+ .optional()
47
+ .describe("TAL UUID. Use list_tals to discover it."),
48
+ };
49
+ export function registerTalTools(server, allowed) {
50
+ if (allowed.has("list_tals")) {
51
+ server.tool("list_tals", "Catalog every Target Account List (TAL): name, purpose, member count, cadence, active state, when last evaluated. TALs are named, criteria-based sets of accounts used by downstream consumers (signal scanner, ABM, outbound sequences, exclusion lists). Use this to discover what TALs exist before invoking show_tal_membership or evaluate_tal_now.", {
52
+ purpose: z
53
+ .string()
54
+ .min(1)
55
+ .optional()
56
+ .describe("Filter to a single purpose ('signal_scanner', 'outbound_sequence', 'abm_advertising', 'exclusion', etc.). Omit to see all."),
57
+ includeInactive: z
58
+ .boolean()
59
+ .optional()
60
+ .describe("Include TALs where is_active=false. Default: false."),
61
+ }, safeTool(async (input) => trpcQuery("mcp.listTals", input)));
62
+ }
63
+ if (allowed.has("show_tal_membership")) {
64
+ server.tool("show_tal_membership", "Show members of one Target Account List with their domain, segment, status, and when each was added. Includes the TAL's full membership criteria (DSL JSON) for context. Identify the TAL by tal_name (preferred) or tal_id. Capped at 100 members by default; pass `limit` for more.", {
65
+ ...talIdentifierShape,
66
+ limit: z
67
+ .number()
68
+ .int()
69
+ .positive()
70
+ .max(500)
71
+ .optional()
72
+ .describe("Max members to return (1–500). Default: 100."),
73
+ }, safeTool(async (input) => trpcQuery("mcp.showTalMembership", input)));
74
+ }
75
+ if (allowed.has("evaluate_tal_now")) {
76
+ server.tool("evaluate_tal_now", "Manually trigger the TAL evaluator for one TAL right now. Re-computes membership against the current resolved record and applies adds/removes (with audit-log events). Use after editing a TAL's criteria, after a bulk enrichment run that may have changed which accounts qualify, or to skip waiting for the next scheduled evaluator pass. Pass `dryRun: true` to preview the changes without writing.", {
77
+ ...talIdentifierShape,
78
+ dryRun: z
79
+ .boolean()
80
+ .optional()
81
+ .describe("If true, classify changes but skip writes. Useful for previewing the impact of a criteria edit. Default: false."),
82
+ }, safeTool(async (input) => trpcMutation("mcp.evaluateTalNow", input)));
83
+ }
84
+ if (allowed.has("create_tal")) {
85
+ server.tool("create_tal", [
86
+ "Create a new Target Account List with a name, purpose, criteria DSL, and refresh cadence.",
87
+ "The TAL starts active but empty — call evaluate_tal_now to populate it.",
88
+ "",
89
+ "Criteria is a tree of conditions evaluated against an account's resolved attributes,",
90
+ "score, lifecycle states, and existing TAL memberships. Operators: AND, OR, NOT. Condition",
91
+ "types: attribute (with operators =, !=, >, >=, <, <=, in, not_in, exists, matches), score",
92
+ "(model + band), lifecycle_state (lifecycle_type + state), member_of (tal_name), exclusion",
93
+ "(tal_name — sugar for NOT member_of).",
94
+ "",
95
+ "Example criteria for 'enterprise US accounts that are enriched and not in the do-not-contact list':",
96
+ "{",
97
+ ' "operator": "AND",',
98
+ ' "conditions": [',
99
+ ' {"type": "attribute", "attribute": "employee_count", "operator": ">=", "value": 500},',
100
+ ' {"type": "attribute", "attribute": "hq_country", "operator": "=", "value": "US"},',
101
+ ' {"type": "lifecycle_state", "lifecycle_type": "enrichment", "state": "enriched"},',
102
+ ' {"type": "exclusion", "tal_name": "do_not_contact"}',
103
+ " ]",
104
+ "}",
105
+ ].join("\n"), {
106
+ talName: z
107
+ .string()
108
+ .min(1)
109
+ .max(200)
110
+ .regex(/^[a-z0-9_]+$/, "tal_name must be snake_case")
111
+ .describe("Snake-case identifier (e.g. 'enterprise_expansion_targets'). Must be unique."),
112
+ description: z
113
+ .string()
114
+ .min(1)
115
+ .describe("One-sentence description of what this list is for and who consumes it."),
116
+ purpose: z
117
+ .enum([
118
+ "signal_scanner",
119
+ "abm_advertising",
120
+ "outbound_sequence",
121
+ "csm_book",
122
+ "event_invite",
123
+ "exclusion",
124
+ ])
125
+ .describe("What the TAL is for. Drives which consumers read it."),
126
+ consumerSystem: z
127
+ .string()
128
+ .optional()
129
+ .describe("Free-text identifier for the system that reads this TAL (e.g. 'signal_scanner', 'gong_engage'). Defaults to the purpose string."),
130
+ criteria: conditionSchema.describe("DSL tree of membership conditions. See tool description for grammar + example."),
131
+ refreshCadence: z
132
+ .enum(["frozen", "weekly", "daily", "hourly"])
133
+ .optional()
134
+ .describe("How often the evaluator should re-compute membership. 'frozen' = never recomputed. Default: weekly."),
135
+ maxSize: z
136
+ .number()
137
+ .int()
138
+ .positive()
139
+ .optional()
140
+ .describe("Hard cap on TAL size. Matches above the cap are deterministically truncated. Omit for no cap."),
141
+ }, safeTool(async (input) => trpcMutation("mcp.createTal", input)));
142
+ }
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopops/mcp-server",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Loop Operations MCP Server — AI skills for RevOps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",