@sellable/mcp 0.1.257 → 0.1.259

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-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -18,7 +18,7 @@ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, reco
18
18
  import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
19
19
  import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
20
20
  import { getCampaignFramework } from "./tools/framework.js";
21
- import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
21
+ import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, listDncEntriesTool, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
22
22
  import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
23
23
  import { getCampaignNavigationState } from "./tools/navigation.js";
24
24
  import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
@@ -375,6 +375,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
375
375
  case "load_csv_dnc_entries":
376
376
  result = await loadCsvDncEntriesTool(args);
377
377
  break;
378
+ case "list_dnc_entries":
379
+ result = await listDncEntriesTool(args);
380
+ break;
378
381
  case "save_domain_filters":
379
382
  result = await saveDomainFilters(args);
380
383
  break;
@@ -1,3 +1,4 @@
1
+ type DncSourceType = "all" | "manual" | "hubspot";
1
2
  type InvalidDncRow = {
2
3
  row: number;
3
4
  column: string | null;
@@ -14,6 +15,41 @@ export type LoadCsvDncEntriesInput = {
14
15
  confirmationToken?: string;
15
16
  confirmed?: boolean;
16
17
  };
18
+ export type ListDncEntriesInput = {
19
+ page?: number;
20
+ limit?: number;
21
+ search?: string;
22
+ listName?: string;
23
+ sourceType?: DncSourceType;
24
+ includeDeleted?: boolean;
25
+ };
26
+ export declare function listDncEntries(input?: ListDncEntriesInput): Promise<{
27
+ ok: boolean;
28
+ workspaceId: string;
29
+ workspaceName: string;
30
+ pagination: {
31
+ total: number;
32
+ page: number;
33
+ limit: number;
34
+ totalPages: number;
35
+ };
36
+ listNames: string[];
37
+ entries: {
38
+ id: string | null;
39
+ type: string;
40
+ domain: string | null;
41
+ linkedin: string | null;
42
+ linkedinUsername: string | null;
43
+ name: string | null;
44
+ source: {
45
+ type: string;
46
+ label: string;
47
+ listName: string | null;
48
+ };
49
+ createdAt: string | null;
50
+ }[];
51
+ guidance: string;
52
+ }>;
17
53
  export declare function loadCsvDncEntries(input: LoadCsvDncEntriesInput): Promise<{
18
54
  guidance: string;
19
55
  createdCount: number;
@@ -14,6 +14,7 @@ const CONFIRMATION_TOKEN_VERSION = "csv-dnc-preview-v1";
14
14
  const confirmationSecret = randomBytes(32).toString("hex");
15
15
  const MAX_DNC_UPLOAD_BYTES = 5 * 1024 * 1024;
16
16
  const MAX_DNC_CANDIDATES = 7500;
17
+ const MAX_DNC_LIST_LIMIT = 100;
17
18
  const STRONG_DOMAIN_HEADER_KEYS = new Set([
18
19
  "domain",
19
20
  "website",
@@ -325,16 +326,84 @@ async function getVerifiedActiveWorkspace() {
325
326
  const config = getConfig();
326
327
  const activeWorkspaceId = config.activeWorkspaceId || config.workspaceId;
327
328
  if (!activeWorkspaceId) {
328
- throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace before importing DNC entries.");
329
+ throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.");
329
330
  }
330
331
  const api = getApi();
331
332
  const { workspaces } = await api.get("/api/v3/workspaces");
332
333
  const match = workspaces.find((workspace) => workspace.id === activeWorkspaceId);
333
334
  if (!match) {
334
- throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the workspace that should receive the DNC import.`);
335
+ throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.`);
335
336
  }
336
337
  return match;
337
338
  }
339
+ function clampListPositiveInt(value, fallback, max) {
340
+ const parsed = typeof value === "number"
341
+ ? value
342
+ : Number.parseInt(String(value ?? ""), 10);
343
+ if (!Number.isFinite(parsed) || parsed < 1)
344
+ return fallback;
345
+ return Math.min(Math.floor(parsed), max);
346
+ }
347
+ function normalizeDncSourceType(value) {
348
+ if (value === undefined || value === null || value === "")
349
+ return "all";
350
+ if (value === "all" || value === "manual" || value === "hubspot") {
351
+ return value;
352
+ }
353
+ throw new Error("sourceType must be one of all, manual, or hubspot.");
354
+ }
355
+ function optionalTrimmed(value) {
356
+ return typeof value === "string" && value.trim() ? value.trim() : null;
357
+ }
358
+ function normalizeCreatedAt(value) {
359
+ if (value instanceof Date)
360
+ return value.toISOString();
361
+ return optionalTrimmed(value);
362
+ }
363
+ function compactDncListEntry(entry) {
364
+ const domain = optionalTrimmed(entry.domain);
365
+ const linkedin = optionalTrimmed(entry.normalizedLinkedinUrl);
366
+ const sourceType = optionalTrimmed(entry.sourceType) ?? "manual";
367
+ return {
368
+ id: optionalTrimmed(entry.id),
369
+ type: domain ? "domain" : "linkedin",
370
+ domain,
371
+ linkedin,
372
+ linkedinUsername: optionalTrimmed(entry.linkedinUsername),
373
+ name: optionalTrimmed(entry.name),
374
+ source: {
375
+ type: sourceType,
376
+ label: optionalTrimmed(entry.sourceLabel) ??
377
+ (sourceType === "hubspot" ? "HubSpot" : "Manual"),
378
+ listName: optionalTrimmed(entry.sourceListName),
379
+ },
380
+ createdAt: normalizeCreatedAt(entry.createdAt),
381
+ };
382
+ }
383
+ function buildDncListPath(input) {
384
+ const page = clampListPositiveInt(input.page, 1, Number.MAX_SAFE_INTEGER);
385
+ const limit = clampListPositiveInt(input.limit, 25, MAX_DNC_LIST_LIMIT);
386
+ const sourceType = normalizeDncSourceType(input.sourceType);
387
+ const params = new URLSearchParams({
388
+ page: String(page),
389
+ limit: String(limit),
390
+ });
391
+ const search = optionalTrimmed(input.search);
392
+ const listName = optionalTrimmed(input.listName);
393
+ if (search)
394
+ params.set("search", search);
395
+ if (listName)
396
+ params.set("listName", listName);
397
+ if (sourceType !== "all")
398
+ params.set("sourceType", sourceType);
399
+ if (input.includeDeleted === true)
400
+ params.set("includeDeleted", "true");
401
+ return {
402
+ path: `/api/v3/dnc-list?${params.toString()}`,
403
+ page,
404
+ limit,
405
+ };
406
+ }
338
407
  function makeSignature(payload) {
339
408
  return createHmac("sha256", confirmationSecret)
340
409
  .update(JSON.stringify(payload))
@@ -556,6 +625,29 @@ async function runDncSpotChecks(entries) {
556
625
  }
557
626
  return checks;
558
627
  }
628
+ export async function listDncEntries(input = {}) {
629
+ const workspace = await getVerifiedActiveWorkspace();
630
+ const api = getApi();
631
+ const request = buildDncListPath(input);
632
+ const response = await api.get(request.path);
633
+ const pagination = response.pagination ?? {};
634
+ return {
635
+ ok: true,
636
+ workspaceId: workspace.id,
637
+ workspaceName: workspace.name,
638
+ pagination: {
639
+ total: pagination.total ?? 0,
640
+ page: pagination.page ?? request.page,
641
+ limit: pagination.limit ?? request.limit,
642
+ totalPages: pagination.totalPages ?? 0,
643
+ },
644
+ listNames: Array.isArray(response.listNames) ? response.listNames : [],
645
+ entries: Array.isArray(response.entries)
646
+ ? response.entries.map(compactDncListEntry)
647
+ : [],
648
+ guidance: "This is Sellable's workspace DNC list used by DNC Check.",
649
+ };
650
+ }
559
651
  export async function loadCsvDncEntries(input) {
560
652
  const workspace = await getVerifiedActiveWorkspace();
561
653
  const api = getApi();
@@ -7,7 +7,6 @@ export type EngagementPost = {
7
7
  name: string;
8
8
  headline: string;
9
9
  profileUrl: string;
10
- followerCount: number | null;
11
10
  };
12
11
  engagement: {
13
12
  likes: number;
@@ -15,16 +14,6 @@ export type EngagementPost = {
15
14
  shares: number;
16
15
  total: number;
17
16
  };
18
- reachSignals: {
19
- targetFollowerMin: number | null;
20
- targetFollowerMax: number | null;
21
- followerBandFit: "in_target_band" | "below_target_band" | "above_target_band" | "unknown";
22
- weightedEngagement: number;
23
- engagementPer1kFollowers: number | null;
24
- weightedEngagementPer1kFollowers: number | null;
25
- reachPenaltyMultiplier: number;
26
- reachAdjustedScore: number | null;
27
- };
28
17
  contentPreview: string;
29
18
  };
30
19
  export type SearchEngagementPostsInput = {
@@ -33,8 +22,6 @@ export type SearchEngagementPostsInput = {
33
22
  maxAgeDays?: number;
34
23
  minTotalEngagement?: number;
35
24
  maxPosts?: number;
36
- targetFollowerMin?: number;
37
- targetFollowerMax?: number;
38
25
  excludePostUrls?: string[];
39
26
  };
40
27
  export type SearchEngagementPostsResponse = {
@@ -76,14 +63,6 @@ export declare const engageDiscoveryToolDefinitions: {
76
63
  type: string;
77
64
  description: string;
78
65
  };
79
- targetFollowerMin: {
80
- type: string;
81
- description: string;
82
- };
83
- targetFollowerMax: {
84
- type: string;
85
- description: string;
86
- };
87
66
  excludePostUrls: {
88
67
  type: string;
89
68
  items: {
@@ -27,14 +27,6 @@ export const engageDiscoveryToolDefinitions = [
27
27
  type: "number",
28
28
  description: "Max posts to return after filtering (default 25).",
29
29
  },
30
- targetFollowerMin: {
31
- type: "number",
32
- description: "Optional lower bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
33
- },
34
- targetFollowerMax: {
35
- type: "number",
36
- description: "Optional upper bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
37
- },
38
30
  excludePostUrls: {
39
31
  type: "array",
40
32
  items: { type: "string" },
@@ -71,101 +63,6 @@ function safeNumber(value) {
71
63
  const n = typeof value === "number" ? value : Number(value);
72
64
  return Number.isFinite(n) ? n : 0;
73
65
  }
74
- function parseFollowerCount(value) {
75
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
76
- return Math.round(value);
77
- }
78
- if (typeof value !== "string")
79
- return null;
80
- const cleaned = value.trim().toLowerCase().replace(/,/g, "");
81
- if (!cleaned)
82
- return null;
83
- const match = cleaned.match(/(\d+(?:\.\d+)?)\s*([kmb])?/);
84
- if (!match)
85
- return null;
86
- const base = Number(match[1]);
87
- if (!Number.isFinite(base) || base <= 0)
88
- return null;
89
- const suffix = match[2] || "";
90
- const multiplier = suffix === "b" ? 1_000_000_000 : suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
91
- return Math.round(base * multiplier);
92
- }
93
- function extractFollowerCount(post) {
94
- const candidates = [
95
- post?.author?.followerCount,
96
- post?.author?.followersCount,
97
- post?.author?.followers,
98
- post?.author?.follower_count,
99
- post?.author?.linkedinFollowers,
100
- post?.author?.stats?.followers,
101
- post?.followerCount,
102
- post?.followersCount,
103
- post?.followers,
104
- post?.authorFollowerCount,
105
- ];
106
- for (const candidate of candidates) {
107
- const parsed = parseFollowerCount(candidate);
108
- if (parsed !== null)
109
- return parsed;
110
- }
111
- return null;
112
- }
113
- function normalizeFollowerBound(value) {
114
- const parsed = parseFollowerCount(value);
115
- return parsed && parsed > 0 ? parsed : null;
116
- }
117
- function followerBandFit(followerCount, targetFollowerMin, targetFollowerMax) {
118
- if (!followerCount)
119
- return "unknown";
120
- if (targetFollowerMin && followerCount < targetFollowerMin) {
121
- return "below_target_band";
122
- }
123
- if (targetFollowerMax && followerCount > targetFollowerMax) {
124
- return "above_target_band";
125
- }
126
- if (targetFollowerMin || targetFollowerMax)
127
- return "in_target_band";
128
- return "unknown";
129
- }
130
- function bandMultiplier(fit, followerCount, targetFollowerMax) {
131
- if (fit === "in_target_band")
132
- return 1;
133
- if (fit === "below_target_band")
134
- return 0.75;
135
- if (fit === "unknown")
136
- return 0.4;
137
- if (!followerCount || !targetFollowerMax)
138
- return 0.45;
139
- if (followerCount <= targetFollowerMax * 2)
140
- return 0.65;
141
- if (followerCount <= targetFollowerMax * 5)
142
- return 0.35;
143
- return 0.15;
144
- }
145
- function reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax) {
146
- const weightedEngagement = engagement.likes + engagement.comments * 4 + engagement.shares * 12;
147
- const fit = followerBandFit(followerCount, targetFollowerMin, targetFollowerMax);
148
- const engagementPer1kFollowers = followerCount
149
- ? Number(((engagement.total / followerCount) * 1000).toFixed(3))
150
- : null;
151
- const weightedEngagementPer1kFollowers = followerCount
152
- ? Number(((weightedEngagement / followerCount) * 1000).toFixed(3))
153
- : null;
154
- const reachPenaltyMultiplier = bandMultiplier(fit, followerCount, targetFollowerMax);
155
- const reachAdjustedScore = weightedEngagementPer1kFollowers === null
156
- ? null
157
- : Number((weightedEngagementPer1kFollowers * reachPenaltyMultiplier).toFixed(3));
158
- return {
159
- targetFollowerMin,
160
- targetFollowerMax,
161
- followerBandFit: fit,
162
- weightedEngagement,
163
- engagementPer1kFollowers,
164
- weightedEngagementPer1kFollowers,
165
- reachPenaltyMultiplier,
166
- reachAdjustedScore,
167
- };
168
- }
169
66
  export async function searchEngagementPosts(input) {
170
67
  const api = getApi();
171
68
  const keywords = (input.keywords || [])
@@ -182,10 +79,6 @@ export async function searchEngagementPosts(input) {
182
79
  const maxPosts = typeof input.maxPosts === "number" && input.maxPosts > 0
183
80
  ? Math.min(50, input.maxPosts)
184
81
  : 25;
185
- const targetFollowerMin = normalizeFollowerBound(input.targetFollowerMin);
186
- const targetFollowerMax = normalizeFollowerBound(input.targetFollowerMax);
187
- const useTargetFollowerBand = Boolean(targetFollowerMin || targetFollowerMax);
188
- const collectLimit = useTargetFollowerBand ? 50 : maxPosts;
189
82
  const exclude = new Set((input.excludePostUrls || [])
190
83
  .map((u) => normalizePostUrl(u))
191
84
  .filter(Boolean));
@@ -194,13 +87,11 @@ export async function searchEngagementPosts(input) {
194
87
  keywords: keywords.map((keyword) => ({ keyword })),
195
88
  page,
196
89
  });
197
- const rawPosts = useTargetFollowerBand && Array.isArray(response?.posts)
198
- ? response.posts
199
- : Array.isArray(response?.topPostsForLLM)
200
- ? response.topPostsForLLM
201
- : Array.isArray(response?.posts)
202
- ? response.posts
203
- : [];
90
+ const rawPosts = Array.isArray(response?.topPostsForLLM)
91
+ ? response.topPostsForLLM
92
+ : Array.isArray(response?.posts)
93
+ ? response.posts
94
+ : [];
204
95
  const now = Date.now();
205
96
  const oldestMs = now - maxAgeDays * 24 * 60 * 60 * 1000;
206
97
  let tooOld = 0;
@@ -220,7 +111,6 @@ export async function searchEngagementPosts(input) {
220
111
  const comments = safeNumber(p?.engagement?.comments);
221
112
  const shares = safeNumber(p?.engagement?.shares);
222
113
  const total = likes + comments + shares;
223
- const engagement = { likes, comments, shares, total };
224
114
  if (total < minTotalEngagement) {
225
115
  tooLowEngagement += 1;
226
116
  continue;
@@ -231,7 +121,6 @@ export async function searchEngagementPosts(input) {
231
121
  tooOld += 1;
232
122
  continue;
233
123
  }
234
- const followerCount = extractFollowerCount(p);
235
124
  kept.push({
236
125
  postId: String(p?.id || ""),
237
126
  url,
@@ -243,31 +132,15 @@ export async function searchEngagementPosts(input) {
243
132
  name: String(p?.author?.name || ""),
244
133
  headline: String(p?.author?.headline || ""),
245
134
  profileUrl: String(p?.author?.profileUrl || ""),
246
- followerCount,
247
135
  },
248
- engagement,
249
- reachSignals: reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax),
136
+ engagement: { likes, comments, shares, total },
250
137
  contentPreview: previewText(String(p?.content || ""), 220),
251
138
  });
252
- if (kept.length >= collectLimit)
139
+ if (kept.length >= maxPosts)
253
140
  break;
254
141
  }
255
- if (useTargetFollowerBand) {
256
- kept.sort((a, b) => {
257
- const scoreDelta = (b.reachSignals.reachAdjustedScore ?? -1) -
258
- (a.reachSignals.reachAdjustedScore ?? -1);
259
- if (scoreDelta !== 0)
260
- return scoreDelta;
261
- return b.engagement.total - a.engagement.total;
262
- });
263
- }
264
- else {
265
- // Sort by total engagement desc for a predictable shortlist.
266
- kept.sort((a, b) => b.engagement.total - a.engagement.total);
267
- }
268
- if (kept.length > maxPosts) {
269
- kept.length = maxPosts;
270
- }
142
+ // Sort by total engagement desc for a predictable shortlist.
143
+ kept.sort((a, b) => b.engagement.total - a.engagement.total);
271
144
  return {
272
145
  success: true,
273
146
  posts: kept,