conduyt-mcp 4.62.0 → 4.63.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
@@ -40,6 +40,7 @@ import { registerPrivacyTools } from "./tools/privacy.js";
40
40
  import { registerSmartViewTools } from "./tools/smart-views.js";
41
41
  import { registerAccountExportTools } from "./tools/account-exports.js";
42
42
  import { registerLifecycleTools } from "./tools/lifecycle.js";
43
+ import { registerLeadPoolTools } from "./tools/lead-pool.js";
43
44
  import { registerReplyCaptureTools } from "./tools/reply-capture.js";
44
45
  import { registerCallFlowTools } from "./tools/call-flows.js";
45
46
  import { registerProjectBlueTools } from "./tools/project-blue.js";
@@ -111,6 +112,7 @@ registerPrivacyTools(server, client);
111
112
  registerSmartViewTools(server, client);
112
113
  registerAccountExportTools(server, client);
113
114
  registerLifecycleTools(server, client);
115
+ registerLeadPoolTools(server, client);
114
116
  registerReplyCaptureTools(server, client);
115
117
  registerCallFlowTools(server, client);
116
118
  registerProjectBlueTools(server, client);
@@ -0,0 +1,27 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytClient } from "../client.js";
3
+ /**
4
+ * (#120, 2026-09-18) the admin-curated lead pool behind Up for Grabs / Lead Pool: settings.poolFilter. When enabled,
5
+ * an unassigned lead is offered only if it matches EVERY configured criterion (tag any-of, source any-of, lead score
6
+ * >= minScore, lead score <= maxScore, ADDED to the CRM within the past createdWithinDays days — the contact's creation time, so a re-import into an existing contact is not new); an empty criterion is
7
+ * skipped; enabled with nothing configured reads as disabled. Claims are gated by the same rules. Tri-surface parity
8
+ * with Settings → Lead routing → Lead Pool and the CLI `lead-pool` commands.
9
+ */
10
+ export interface PoolFilterShape {
11
+ enabled: boolean;
12
+ tagIds: string[];
13
+ sources: string[];
14
+ minScore: number | null;
15
+ maxScore: number | null;
16
+ createdWithinDays: number | null;
17
+ }
18
+ /** the stored filter as the API's zod shape (unknown / legacy keys tolerated) */
19
+ export declare function poolFilterOf(settings: Record<string, unknown>): PoolFilterShape;
20
+ export type TagRef = {
21
+ id: string;
22
+ name: string | null;
23
+ };
24
+ /** the same plain-English criteria the Up for Grabs page prints under its header (lib/pool-filter-summary.ts in the app):
25
+ * a deleted tag (name null) stays visible because its id still filters every lead out; a failed lookup is said so */
26
+ export declare function poolCriteriaPhrases(filter: PoolFilterShape, tags: TagRef[], tagLookupFailed?: boolean): string[];
27
+ export declare function registerLeadPoolTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ import { formatResult } from "../client.js";
3
+ import { patchGeneralSettings } from "./lifecycle.js";
4
+ const EMPTY = { enabled: false, tagIds: [], sources: [], minScore: null, maxScore: null, createdWithinDays: null };
5
+ function intOrNull(v) {
6
+ return typeof v === "number" && Number.isInteger(v) ? v : null;
7
+ }
8
+ /** the stored filter as the API's zod shape (unknown / legacy keys tolerated) */
9
+ export function poolFilterOf(settings) {
10
+ const raw = settings.poolFilter;
11
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
12
+ return { ...EMPTY };
13
+ const r = raw;
14
+ const minScore = intOrNull(r.minScore);
15
+ return {
16
+ enabled: r.enabled === true,
17
+ tagIds: Array.isArray(r.tagIds) ? r.tagIds.filter((t) => typeof t === "string") : [],
18
+ sources: Array.isArray(r.sources) ? r.sources.filter((s) => typeof s === "string") : [],
19
+ minScore: minScore !== null && minScore > 0 ? minScore : null,
20
+ maxScore: intOrNull(r.maxScore),
21
+ createdWithinDays: intOrNull(r.createdWithinDays),
22
+ };
23
+ }
24
+ const list = (items) => items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
25
+ /** the same plain-English criteria the Up for Grabs page prints under its header (lib/pool-filter-summary.ts in the app):
26
+ * a deleted tag (name null) stays visible because its id still filters every lead out; a failed lookup is said so */
27
+ export function poolCriteriaPhrases(filter, tags, tagLookupFailed = false) {
28
+ if (!filter.enabled)
29
+ return [];
30
+ const out = [];
31
+ if (filter.sources.length > 0)
32
+ out.push(`from ${list(filter.sources)}`);
33
+ if (filter.createdWithinDays !== null)
34
+ out.push(filter.createdWithinDays === 1 ? "added in the past day" : `added in the past ${filter.createdWithinDays} days`);
35
+ if (tags.length > 0) {
36
+ if (tagLookupFailed) {
37
+ out.push(`tagged one of ${tags.length} ${tags.length === 1 ? "tag" : "tags"} (names unavailable right now)`);
38
+ }
39
+ else {
40
+ const names = tags.filter((t) => t.name !== null).map((t) => t.name);
41
+ const deleted = tags.length - names.length;
42
+ if (deleted > 0)
43
+ names.push(deleted === 1 ? "a deleted tag (matches nothing)" : `${deleted} deleted tags (match nothing)`);
44
+ out.push(`tagged ${list(names)}`);
45
+ }
46
+ }
47
+ if (filter.minScore !== null && filter.maxScore !== null)
48
+ out.push(filter.minScore === filter.maxScore ? `lead score ${filter.minScore}` : `lead score ${filter.minScore} to ${filter.maxScore}`);
49
+ else if (filter.minScore !== null)
50
+ out.push(`lead score ${filter.minScore} or higher`);
51
+ else if (filter.maxScore !== null)
52
+ out.push(`lead score ${filter.maxScore} or lower`);
53
+ return out;
54
+ }
55
+ /** every configured id, resolved to a name where the tag still exists; the lookup is display-only and never throws */
56
+ async function tagNamesFor(client, tagIds) {
57
+ if (tagIds.length === 0)
58
+ return { tags: [], tagLookupFailed: false };
59
+ try {
60
+ const result = (await client.get("/api/v1/tags?per_page=200"));
61
+ const rows = Array.isArray(result) ? result : Array.isArray(result.data) ? (result.data) : [];
62
+ const byId = new Map(rows.filter((t) => !!t && typeof t === "object" && typeof t.id === "string").map((t) => [t.id, t.name]));
63
+ return { tags: tagIds.map((id) => ({ id, name: byId.get(id) ?? null })), tagLookupFailed: false };
64
+ }
65
+ catch {
66
+ return { tags: tagIds.map((id) => ({ id, name: null })), tagLookupFailed: true };
67
+ }
68
+ }
69
+ const score = z.number().int().min(0).max(1000);
70
+ export function registerLeadPoolTools(server, client) {
71
+ server.tool("conduyt_get_lead_pool_settings", "Read the admin-curated lead pool behind Up for Grabs / Lead Pool (settings.poolFilter + poolClaimDailyLimit). When enabled, an unassigned lead is offered only if it matches EVERY configured criterion: tagIds (any of), sources (any of), minScore (lead score >=), maxScore (lead score <=; min = max keeps one exact score), createdWithinDays (added to the CRM in the past N days: the contact's creation time — import, webhook, form, API or by hand; a re-import into an existing contact does not make it new). An empty criterion is skipped; enabled with nothing configured reads as disabled. Claims are gated by the same rules. tags carries the resolved tag names; criteria is the plain-English line the Up for Grabs page shows. Requires the settings scope.", {}, async () => {
72
+ const result = (await client.get("/api/v1/settings"));
73
+ const raw = result?.data ?? {};
74
+ const filter = poolFilterOf(raw);
75
+ const { tags, tagLookupFailed } = await tagNamesFor(client, filter.tagIds);
76
+ const poolClaimDailyLimit = intOrNull(raw.poolClaimDailyLimit);
77
+ return formatResult({ data: { ...filter, tags, ...(tagLookupFailed ? { tagLookupFailed } : {}), criteria: poolCriteriaPhrases(filter, tags, tagLookupFailed), poolClaimDailyLimit } });
78
+ });
79
+ server.tool("conduyt_update_lead_pool_settings", "Change the admin-curated lead pool (Up for Grabs / Lead Pool). Pass only what changes; every other criterion keeps its stored value. Criteria are ANDed: enabled, tagIds (contact has ANY of these tags; [] = no tag rule), sources (lead source is ANY of these; [] = no source rule), minScore / maxScore (lead score floor / ceiling, 0..1000, null = none; the same value for both keeps one exact score, e.g. \"lead quality 4\"), createdWithinDays (only leads ADDED to the CRM in the past N days — creation time, a re-import of an existing contact is not new; 1..365, null = none), poolClaimDailyLimit (0..200 grabs per agent per day, 0 = no cap). Enabling with no criterion at all is refused (it would read as disabled). maxScore below minScore is refused. Sends the settings revision it read; a 409 (another save landed first) returns the current settings to re-derive from. Requires settings:edit.", {
80
+ enabled: z.boolean().optional().describe("Turn the curated pool on or off"),
81
+ tagIds: z.array(z.string().uuid()).max(50).optional().describe("Tag UUIDs the lead must carry one of (conduyt_list_tags); [] clears the tag rule"),
82
+ sources: z.array(z.string().trim().min(1).max(100)).max(50).optional().describe("Lead sources, exact match, any of; [] clears the source rule"),
83
+ minScore: score.nullable().optional().describe("Lead score floor (>=); null clears"),
84
+ maxScore: score.nullable().optional().describe("Lead score ceiling (<=); null clears; equal to minScore = one exact score"),
85
+ createdWithinDays: z.number().int().min(1).max(365).nullable().optional().describe("Only leads added to the CRM (created) in the past N days; null clears"),
86
+ poolClaimDailyLimit: z.number().int().min(0).max(200).optional().describe("Daily self-serve claim cap per agent; 0 = off"),
87
+ }, async (params) => {
88
+ const current = (await client.get("/api/v1/settings"));
89
+ const stored = poolFilterOf(current?.data ?? {});
90
+ const next = {
91
+ enabled: params.enabled ?? stored.enabled,
92
+ tagIds: params.tagIds ?? stored.tagIds,
93
+ sources: params.sources ?? stored.sources,
94
+ minScore: params.minScore === undefined ? stored.minScore : params.minScore,
95
+ maxScore: params.maxScore === undefined ? stored.maxScore : params.maxScore,
96
+ createdWithinDays: params.createdWithinDays === undefined ? stored.createdWithinDays : params.createdWithinDays,
97
+ };
98
+ if (next.minScore !== null && next.maxScore !== null && next.maxScore < next.minScore) {
99
+ throw new Error(`maxScore (${next.maxScore}) must be at least minScore (${next.minScore})`);
100
+ }
101
+ const hasCriterion = next.tagIds.length > 0 || next.sources.length > 0 || next.minScore !== null || next.maxScore !== null || next.createdWithinDays !== null;
102
+ if (next.enabled && !hasCriterion) {
103
+ throw new Error("enabled with no criterion would read as disabled — pass at least one of tagIds, sources, minScore, maxScore, createdWithinDays, or set enabled=false");
104
+ }
105
+ const body = { poolFilter: next };
106
+ if (params.poolClaimDailyLimit !== undefined)
107
+ body.poolClaimDailyLimit = params.poolClaimDailyLimit;
108
+ return patchGeneralSettings(client, body);
109
+ });
110
+ server.tool("conduyt_list_up_for_grabs", "The Up for Grabs queue as the rep-facing page shows it: open claim offers plus the implicit pool of unassigned leads the account's curated pool filter admits (highest lead score first, up to 200), each with a teaser (name, company, source, score, city/state; phone and email only for admin keys) and eligibleCount. poolFilter is the active curation (tags resolved to names) and criteria the plain-English line under the page header — empty when curation is off. Claim with POST /api/v1/up-for-grabs/{contactId}/claim. Requires the up-for-grabs scope on the key.", {}, async () => {
111
+ const result = (await client.get("/api/v1/up-for-grabs"));
112
+ const data = result?.data ?? {};
113
+ const pf = data.poolFilter && typeof data.poolFilter === "object" ? data.poolFilter : null;
114
+ const filter = pf
115
+ ? { enabled: pf.enabled === true, tagIds: (pf.tags ?? []).map((t) => t.id), sources: pf.sources ?? [], minScore: pf.minScore ?? null, maxScore: pf.maxScore ?? null, createdWithinDays: pf.createdWithinDays ?? null }
116
+ : { ...EMPTY };
117
+ const offers = Array.isArray(data.offers) ? data.offers : [];
118
+ return formatResult({ data: { offers, count: offers.length, poolFilter: pf, criteria: poolCriteriaPhrases(filter, pf?.tags ?? [], pf?.tagLookupFailed === true) } });
119
+ });
120
+ }
@@ -1,3 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { ConduytClient } from "../client.js";
3
+ /** (default columns r18, Codex r17 h1) every general-settings PATCH names the revision it loaded: read it, save, and on a 409 hand
4
+ * the conflict back with the current settings so the caller re-derives its change (tri-surface parity with the browser) */
5
+ export declare function patchGeneralSettings(client: ConduytClient, body: Record<string, unknown>): Promise<{
6
+ content: Array<{
7
+ type: "text";
8
+ text: string;
9
+ }>;
10
+ }>;
3
11
  export declare function registerLifecycleTools(server: McpServer, client: ConduytClient): void;
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { ConduytApiError, formatResult } from "../client.js";
3
3
  /** (default columns r18, Codex r17 h1) every general-settings PATCH names the revision it loaded: read it, save, and on a 409 hand
4
4
  * the conflict back with the current settings so the caller re-derives its change (tri-surface parity with the browser) */
5
- async function patchGeneralSettings(client, body) {
5
+ export async function patchGeneralSettings(client, body) {
6
6
  const read = async () => {
7
7
  const result = (await client.get("/api/v1/settings"));
8
8
  return typeof result?.data?.settingsRevision === "string" ? result.data.settingsRevision : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.62.0",
3
+ "version": "4.63.0",
4
4
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-09-16T16:47:18.079Z",
3
- "toolCount": 277,
2
+ "generatedAt": "2026-09-18T04:50:57.467Z",
3
+ "toolCount": 280,
4
4
  "totalEndpoints": 984,
5
5
  "counts": {
6
- "covered": 259,
6
+ "covered": 260,
7
7
  "excluded": 289,
8
- "planned": 436,
8
+ "planned": 435,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -3622,6 +3622,8 @@
3622
3622
  "tools": [
3623
3623
  "conduyt_get_contact_table_defaults",
3624
3624
  "conduyt_set_contact_table_defaults",
3625
+ "conduyt_get_lead_pool_settings",
3626
+ "conduyt_update_lead_pool_settings",
3625
3627
  "conduyt_get_lifecycle_settings",
3626
3628
  "conduyt_update_agent_statuses"
3627
3629
  ]
@@ -4174,8 +4176,10 @@
4174
4176
  "reason": "team member listing alias"
4175
4177
  },
4176
4178
  "GET /api/v1/up-for-grabs": {
4177
- "status": "planned",
4178
- "reason": "up-for-grabs claim surfaces"
4179
+ "status": "covered",
4180
+ "tools": [
4181
+ "conduyt_list_up_for_grabs"
4182
+ ]
4179
4183
  },
4180
4184
  "POST /api/v1/up-for-grabs/:contactId/claim": {
4181
4185
  "status": "planned",