@sellable/mcp 0.1.55 → 0.1.57

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.
@@ -314,6 +314,7 @@ export declare const leadToolDefinitions: ({
314
314
  filters: {
315
315
  type: string;
316
316
  description: string;
317
+ items?: undefined;
317
318
  properties?: undefined;
318
319
  };
319
320
  companyFilters: {
@@ -473,6 +474,59 @@ export declare const leadToolDefinitions: ({
473
474
  filters: {
474
475
  type: string;
475
476
  description: string;
477
+ items: {
478
+ type: string;
479
+ properties: {
480
+ type: {
481
+ type: string;
482
+ };
483
+ values: {
484
+ type: string;
485
+ items: {
486
+ type: string;
487
+ properties: {
488
+ id: {
489
+ type: string;
490
+ };
491
+ text: {
492
+ type: string;
493
+ };
494
+ selectionType: {
495
+ type: string;
496
+ enum: string[];
497
+ };
498
+ parent: {
499
+ type: string;
500
+ properties: {
501
+ id: {
502
+ type: string;
503
+ };
504
+ };
505
+ };
506
+ };
507
+ required: string[];
508
+ };
509
+ };
510
+ rangeValue: {
511
+ type: string;
512
+ properties: {
513
+ min: {
514
+ type: string;
515
+ };
516
+ max: {
517
+ type: string;
518
+ };
519
+ };
520
+ required: string[];
521
+ };
522
+ selectedSubFilter: {
523
+ anyOf: {
524
+ type: string;
525
+ }[];
526
+ };
527
+ };
528
+ required: string[];
529
+ };
476
530
  properties?: undefined;
477
531
  };
478
532
  keywords: {
@@ -1347,6 +1401,7 @@ export declare const leadToolDefinitions: ({
1347
1401
  };
1348
1402
  };
1349
1403
  };
1404
+ items?: undefined;
1350
1405
  };
1351
1406
  page: {
1352
1407
  type: string;
@@ -2015,6 +2070,7 @@ export declare const leadToolDefinitions: ({
2015
2070
  })[];
2016
2071
  export declare function searchApollo(input: ApolloSearchInput): Promise<unknown>;
2017
2072
  export declare function searchSalesNav(input: SalesNavSearchInput): Promise<unknown>;
2073
+ export declare function normalizeSalesNavSearchInput(input: SalesNavSearchInput): SalesNavSearchInput;
2018
2074
  export declare function searchProspeo(input: ProspeoSearchInput): Promise<any>;
2019
2075
  export declare function loadCsvDomains(input: LoadCsvDomainsInput): Promise<{
2020
2076
  ok: boolean;
@@ -32,6 +32,46 @@ const prospeoFilterValueSchema = {
32
32
  exclude: { type: "array", items: { type: "string" } },
33
33
  },
34
34
  };
35
+ const salesNavFilterValueSchema = {
36
+ type: "object",
37
+ properties: {
38
+ id: { type: "string" },
39
+ text: { type: "string" },
40
+ selectionType: {
41
+ type: "string",
42
+ enum: ["INCLUDED", "EXCLUDED"],
43
+ },
44
+ parent: {
45
+ type: "object",
46
+ properties: {
47
+ id: { type: "string" },
48
+ },
49
+ },
50
+ },
51
+ required: ["id", "text", "selectionType"],
52
+ };
53
+ const salesNavFilterSchema = {
54
+ type: "object",
55
+ properties: {
56
+ type: { type: "string" },
57
+ values: {
58
+ type: "array",
59
+ items: salesNavFilterValueSchema,
60
+ },
61
+ rangeValue: {
62
+ type: "object",
63
+ properties: {
64
+ min: { type: "number" },
65
+ max: { type: "number" },
66
+ },
67
+ required: ["min", "max"],
68
+ },
69
+ selectedSubFilter: {
70
+ anyOf: [{ type: "string" }, { type: "number" }],
71
+ },
72
+ },
73
+ required: ["type"],
74
+ };
35
75
  const prospeoRangeFilterSchema = {
36
76
  type: "object",
37
77
  description: "Numeric range filter (min/max)",
@@ -540,7 +580,8 @@ export const leadToolDefinitions = [
540
580
  properties: {
541
581
  filters: {
542
582
  type: "array",
543
- description: "Sales Nav filter array",
583
+ description: "Sales Nav filter array. Pass filter objects, not JSON strings.",
584
+ items: salesNavFilterSchema,
544
585
  },
545
586
  keywords: {
546
587
  type: "string",
@@ -1312,7 +1353,30 @@ export async function searchSalesNav(input) {
1312
1353
  campaignOfferId: input?.campaignOfferId,
1313
1354
  });
1314
1355
  const api = getApi();
1315
- return api.post(`/api/v3/sales-nav/search`, input);
1356
+ return api.post(`/api/v3/sales-nav/search`, normalizeSalesNavSearchInput(input));
1357
+ }
1358
+ export function normalizeSalesNavSearchInput(input) {
1359
+ return {
1360
+ ...input,
1361
+ filters: normalizeSalesNavFilters(input.filters),
1362
+ };
1363
+ }
1364
+ function normalizeSalesNavFilters(filters) {
1365
+ return filters.map((filter, index) => {
1366
+ if (typeof filter !== "string") {
1367
+ return filter;
1368
+ }
1369
+ try {
1370
+ const parsed = JSON.parse(filter);
1371
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1372
+ throw new Error("not an object");
1373
+ }
1374
+ return parsed;
1375
+ }
1376
+ catch {
1377
+ throw new Error(`Invalid Sales Nav filter at index ${index}. Pass filter objects, not JSON strings.`);
1378
+ }
1379
+ });
1316
1380
  }
1317
1381
  export async function searchProspeo(input) {
1318
1382
  if (input?.campaignOfferId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -423,7 +423,7 @@ should test for this campaign. Those can run in parallel and usually take
423
423
  batching is available, batch independent tool reads/lookups only. If real
424
424
  parallel execution is not available or not allowed, run the same DAG
425
425
  sequentially and use honest copy: `I’ll tighten the filter first, then draft
426
- the message from the same sample.` Never say `kicking off two workstreams`,
426
+ the message from the same sample.` Never say `kicking off two workstreams`,
427
427
  `in parallel`, or `background` unless parallel branches were actually
428
428
  launched.
429
429
  - Never run a downstream stage until the active `flow.v2.json` step's
@@ -574,14 +574,25 @@ Required behavior:
574
574
  - Branch C: Prospeo / domains only when the campaign has a domain/account
575
575
  path or the user supplied domains. Estimate email/contact scale and call out
576
576
  weaker LinkedIn activity.
577
- If the host cannot run these branches in parallel, run them sequentially and
578
- do not claim they ran in parallel. If only one source angle is credible, say
579
- that and run the best primary source plus one cheap fallback/quality check
580
- when available. For Signals-first campaigns, search multiple Signals keyword
581
- lanes and fetch top-post engagers in parallel when tool batching allows it.
582
- For Sales Nav-first or Prospeo/account-first campaigns, run multiple preview
583
- lanes for that provider in parallel and use Signals only as a warmth/quality
584
- check when relevant
577
+ If the host cannot run these branches in parallel, run them sequentially and
578
+ do not claim they ran in parallel. If only one source angle is credible, say
579
+ that and run the best primary source plus one cheap fallback/quality check
580
+ when available. For Signals-first campaigns, search multiple Signals keyword
581
+ lanes and fetch top-post engagers in parallel when tool batching allows it.
582
+ For Sales Nav-first or Prospeo/account-first campaigns, run multiple preview
583
+ lanes for that provider in parallel and use Signals only as a warmth/quality
584
+ check when relevant
585
+ - after every Sales Nav preview, validate that filters actually applied before
586
+ using the lane in `lead-review.md`: `searchUrl` should include filters, the
587
+ first page should match the intended roles/companies, and the result count
588
+ should be plausible. If Sales Nav returns a giant unfiltered pool, drops the
589
+ filters, or errors after one clean retry, call it out as a provider/tool
590
+ issue and do not recommend Sales Nav.
591
+ - when the brief or user names target roles, Sales Nav must preserve those role
592
+ names through `CURRENT_TITLE` lookups. Do not use broad seniority as a
593
+ substitute for role intent. VP Sales, VP Revenue, CRO, Head of Growth, and
594
+ similar targets should stay visible in the filter recipe and the source
595
+ explanation.
585
596
  - for Signals-first campaigns, raw post search volume is only inventory, not
586
597
  lead volume. `492 post results` means matching LinkedIn posts found across
587
598
  keyword lanes; it does not mean 492 prospects. The source decision must name
@@ -40,7 +40,7 @@ must contain:
40
40
  - a plain confirmation that the campaign was created and the approved brief was
41
41
  saved
42
42
  - a one-line orientation sentence: `Open this to watch lead sourcing, rubric
43
- scoring, and messaging populate live.`
43
+ scoring, and messaging populate live.`
44
44
  - the `watchUrl` captured from the `create_campaign` response
45
45
 
46
46
  Example skeleton:
@@ -275,6 +275,10 @@ Use first when LinkedIn activity plus tighter role / company filters matter.
275
275
 
276
276
  - Always call `lookup_sales_nav_filter` before dynamic filters.
277
277
  - Start with a broad-but-reasonable baseline: company size + core roles + core industries.
278
+ - If the campaign gives specific target role names, preserve them with
279
+ `CURRENT_TITLE` lookups. Seniority filters are supporting constraints, not a
280
+ substitute for target roles. Example: VP Sales / VP Revenue / Head of Growth
281
+ should use title filters, not only `SENIORITY_LEVEL: Vice President`.
278
282
  - For reply-likelihood-first outbound, Sales Nav is the second choice after Signals: use it when the TAM is not active enough on LinkedIn, when Signals cannot sustain enough good fits, or when the targeting thesis depends on tighter role/company control than Signals can provide.
279
283
  - For InMail or LinkedIn-send motions, establish the baseline TAM first, then test a `POSTED_ON_LINKEDIN` slice when the pool can still sustain a campaign.
280
284
  - Treat recent posters as a preferred first-send slice, not just a nice-to-have proxy. When the recently-posted slice still yields enough projected good fits, prefer it because reply / acceptance performance is usually materially better than the cold full-TAM pool.
@@ -293,6 +297,13 @@ Use first when LinkedIn activity plus tighter role / company filters matter.
293
297
  - When you fall back from the recently-posted slice, say clearly that the posted filter was tested, explain why it was dropped, and keep the best non-posted lane as the source of truth.
294
298
  - Use `RECENTLY_CHANGED_JOBS` when job-change activity is part of the targeting thesis.
295
299
  - If quality is poor, tighten the lane with role, industry, seniority, geography, or activity filters.
300
+ - After each Sales Nav preview, sanity-check the result before using it in the
301
+ decision: the returned `searchUrl` should include filters, the first page
302
+ should visibly match the intended roles and companies, and the count should
303
+ be plausible. If filters did not apply, the search errors, or the total looks
304
+ obviously unfiltered, retry once with clean filter objects. If it still fails,
305
+ mark Sales Nav as a provider/tool issue and do not include it as a winning
306
+ source.
296
307
  - If quality is good but scale is too small, widen the lane by adding/removing roles, expanding industries, widening headcount bands, or relaxing activity constraints.
297
308
  - Use as many smart refinement steps as needed within the remaining probe budget instead of returning the first under-scaled recipe.
298
309
  - If the lane is meant to support scalable outbound, do not stop at a merely borderline workable result when obvious expansion steps remain.
@@ -50,6 +50,13 @@ Example search name: "Active CXOs at 201-500 companies"
50
50
  2. LOOKUP DYNAMIC FILTERS - Get IDs for geography, industry, titles, etc.
51
51
  Before using ANY dynamic filter, you MUST call lookup_sales_nav_filter.
52
52
 
53
+ When the campaign names target roles, use CURRENT_TITLE lookups for those
54
+ role names. Seniority alone is not enough. For example, "VP Sales / VP
55
+ Revenue / Head of Growth" should become title filters plus seniority, not
56
+ only `SENIORITY_LEVEL: Vice President`, because generic VP seniority pulls
57
+ business development, partnerships, marketing, operations, and other noisy
58
+ roles.
59
+
53
60
  Dynamic filters (REQUIRE lookup):
54
61
 
55
62
  - REGION: "United States", "California", "London"
@@ -114,6 +121,7 @@ NEVER do these:
114
121
 
115
122
  <filter_format>
116
123
  ALL filter values MUST include id, text, and selectionType.
124
+ Pass `filters` as an array of objects. Do not stringify filters as JSON.
117
125
 
118
126
  CORRECT:
119
127
 
@@ -145,6 +153,21 @@ WRONG:
145
153
 
146
154
  </filter_format>
147
155
 
156
+ <result_validation>
157
+ After every `search_sales_nav` call, sanity-check the returned result before
158
+ using it in a source decision:
159
+
160
+ - `searchUrl` should include a `filters:List(...)` query, not only `keywords`.
161
+ - The sample rows should visibly match the intended role/company lane.
162
+ - The total result count should be plausible for the filters used.
163
+
164
+ If filters did not apply, the search errors, or the result count is obviously
165
+ unfiltered, do not present Sales Nav as a valid lane. Retry once with clean
166
+ filter objects if the issue was formatting. If it still fails, say plainly:
167
+ "Sales Nav did not apply the filters cleanly, so I’m not using it for this
168
+ decision." Then continue with Signals or Prospeo if they have usable evidence.
169
+ </result_validation>
170
+
148
171
  <static_filters>
149
172
  These filters need NO lookup - use these IDs directly:
150
173