@sellable/mcp 0.1.225 → 0.1.227

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.
@@ -63,6 +63,47 @@ const prospeoCompanySearchTokenRefs = new Map();
63
63
  let prospeoCompanySearchTokenRefCounter = 0;
64
64
  const PROSPEO_COMPANY_SEARCH_TOKEN_REF_PREFIX = "mcp-prospeo-company-search-token:";
65
65
  const MAX_PROSPEO_COMPANY_SEARCH_TOKEN_REFS = 200;
66
+ const PROSPEO_LARGE_TITLE_INCLUDE_BOOLEAN_THRESHOLD = 8;
67
+ const prospeoCompanyKeywordIncludeAliases = new Map([
68
+ ["ai", "artificial intelligence"],
69
+ ["api", "application programming interface"],
70
+ ["gtm", "go to market"],
71
+ ["saas", "software as a service"],
72
+ ]);
73
+ const prospeoCompanyIcpRegionAliases = new Map([
74
+ ["north america", ["United States", "Canada"]],
75
+ ["na", ["United States", "Canada"]],
76
+ ["us", ["United States"]],
77
+ ["usa", ["United States"]],
78
+ ["u.s.", ["United States"]],
79
+ ["u.s.a.", ["United States"]],
80
+ ]);
81
+ const prospeoCompanyIcpDepartments = new Set([
82
+ "Consumers",
83
+ "Customer Success",
84
+ "Data",
85
+ "Design",
86
+ "Engineering",
87
+ "Finance",
88
+ "HR",
89
+ "IT",
90
+ "Legal",
91
+ "Marketing",
92
+ "Operations",
93
+ "Procurement",
94
+ "SMB Owners",
95
+ "Sales",
96
+ "Security",
97
+ ]);
98
+ const prospeoKeyCustomerCompanionFilters = [
99
+ "company_attributes",
100
+ "company_headcount_range",
101
+ "company_headcount_custom",
102
+ "company_industry",
103
+ "company_keywords",
104
+ "company_website_search",
105
+ "company_icp",
106
+ ];
66
107
  const prospeoFilterValueSchema = {
67
108
  type: "object",
68
109
  description: "Include/exclude list filter (values must match Prospeo enums)",
@@ -2363,11 +2404,12 @@ function removeUndefinedValues(input) {
2363
2404
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
2364
2405
  }
2365
2406
  function normalizeProspeoCompanySearchInputForMcp(input) {
2407
+ const seedCompanies = normalizeMcpSeeds(input.seedCompanies, "company");
2408
+ const seedDomains = normalizeMcpSeeds(input.seedDomains, "domain");
2366
2409
  const filters = input?.filters && typeof input.filters === "object"
2367
2410
  ? clonePlainObject(input.filters)
2368
2411
  : {};
2369
- const hasSeeds = (input.seedCompanies?.length ?? 0) > 0 ||
2370
- (input.seedDomains?.length ?? 0) > 0;
2412
+ const hasSeeds = seedCompanies.length > 0 || seedDomains.length > 0;
2371
2413
  if (hasSeeds) {
2372
2414
  const lookalike = filters.company_lookalike;
2373
2415
  if (lookalike &&
@@ -2375,13 +2417,233 @@ function normalizeProspeoCompanySearchInputForMcp(input) {
2375
2417
  !Array.isArray(lookalike) &&
2376
2418
  "company_oids" in lookalike) {
2377
2419
  delete lookalike.company_oids;
2420
+ removeEmptyObjectFilter(filters, "company_lookalike");
2378
2421
  }
2379
2422
  }
2423
+ normalizeMcpCompanyKeywords(filters);
2424
+ normalizeMcpCompanyWebsiteSearch(filters);
2425
+ normalizeMcpCompanyIcp(filters);
2426
+ stripMcpDuplicateCompanyIndustry(filters);
2427
+ stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds);
2428
+ stripMcpKeyCustomerCompanionFilters(filters);
2380
2429
  return {
2381
2430
  ...input,
2431
+ seedCompanies: seedCompanies.length > 0 ? seedCompanies : undefined,
2432
+ seedDomains: seedDomains.length > 0 ? seedDomains : undefined,
2382
2433
  filters,
2383
2434
  };
2384
2435
  }
2436
+ function normalizeProspeoSearchInputForMcp(input) {
2437
+ const filters = input?.filters && typeof input.filters === "object"
2438
+ ? clonePlainObject(input.filters)
2439
+ : {};
2440
+ normalizeMcpPersonJobTitle(filters);
2441
+ return {
2442
+ ...input,
2443
+ filters,
2444
+ };
2445
+ }
2446
+ function normalizeMcpCompanyKeywords(filters) {
2447
+ const keywords = filters.company_keywords;
2448
+ if (!isPlainObject(keywords)) {
2449
+ return;
2450
+ }
2451
+ const include = normalizeStringArray(keywords.include).map((keyword) => {
2452
+ const alias = prospeoCompanyKeywordIncludeAliases.get(keyword.toLowerCase());
2453
+ return alias ?? keyword;
2454
+ });
2455
+ const validInclude = include.filter((keyword) => keyword.length >= 3);
2456
+ if (validInclude.length > 0) {
2457
+ keywords.include = uniqueStrings(validInclude);
2458
+ }
2459
+ else {
2460
+ delete keywords.include;
2461
+ delete keywords.exclude;
2462
+ }
2463
+ if (keywords.exclude !== undefined && validInclude.length === 0) {
2464
+ delete keywords.exclude;
2465
+ }
2466
+ removeEmptyObjectFilter(filters, "company_keywords");
2467
+ }
2468
+ function normalizeMcpCompanyWebsiteSearch(filters) {
2469
+ const websiteSearch = filters.company_website_search;
2470
+ if (!isPlainObject(websiteSearch)) {
2471
+ return;
2472
+ }
2473
+ const includeKeywords = normalizeStringArray(websiteSearch.include_keywords);
2474
+ const hasPositiveWebsiteSignal = includeKeywords.length > 0 ||
2475
+ normalizeStringArray(websiteSearch.url_contains).length > 0 ||
2476
+ normalizeStringArray(websiteSearch.urls).length > 0 ||
2477
+ Object.entries(websiteSearch).some(([key, value]) => key.startsWith("has_") && typeof value === "boolean" && value);
2478
+ if (!hasPositiveWebsiteSignal) {
2479
+ delete websiteSearch.exclude_keywords;
2480
+ }
2481
+ removeEmptyObjectFilter(filters, "company_website_search");
2482
+ }
2483
+ function normalizeMcpCompanyIcp(filters) {
2484
+ const icp = filters.company_icp;
2485
+ if (!isPlainObject(icp)) {
2486
+ return;
2487
+ }
2488
+ const scope = typeof icp.geographic_scope === "string"
2489
+ ? icp.geographic_scope.trim()
2490
+ : "";
2491
+ const scopeMarkets = expandMcpCompanyIcpMarket(scope);
2492
+ if (scopeMarkets.length > 0) {
2493
+ icp.geographic_scope = "multi_country";
2494
+ icp.geographic_markets = uniqueStrings([
2495
+ ...scopeMarkets,
2496
+ ...normalizeMcpCompanyIcpMarkets(icp.geographic_markets),
2497
+ ]);
2498
+ }
2499
+ else if (scope &&
2500
+ scope !== "single_country" &&
2501
+ scope !== "multi_country") {
2502
+ delete icp.geographic_scope;
2503
+ }
2504
+ if (icp.geographic_markets !== undefined) {
2505
+ icp.geographic_markets = normalizeMcpCompanyIcpMarkets(icp.geographic_markets);
2506
+ if (Array.isArray(icp.geographic_markets) &&
2507
+ icp.geographic_markets.length === 0) {
2508
+ delete icp.geographic_markets;
2509
+ }
2510
+ }
2511
+ normalizeMcpCompanyIcpDepartments(icp);
2512
+ removeEmptyObjectFilter(filters, "company_icp");
2513
+ }
2514
+ function normalizeMcpCompanyIcpDepartments(icp) {
2515
+ const departments = icp.departments;
2516
+ if (Array.isArray(departments)) {
2517
+ const valid = departments
2518
+ .filter((department) => typeof department === "string")
2519
+ .filter((department) => prospeoCompanyIcpDepartments.has(department));
2520
+ if (valid.length > 0) {
2521
+ icp.departments = uniqueStrings(valid);
2522
+ }
2523
+ else {
2524
+ delete icp.departments;
2525
+ }
2526
+ return;
2527
+ }
2528
+ if (!isPlainObject(departments)) {
2529
+ return;
2530
+ }
2531
+ const include = normalizeStringArray(departments.include).filter((department) => prospeoCompanyIcpDepartments.has(department));
2532
+ const exclude = normalizeStringArray(departments.exclude).filter((department) => prospeoCompanyIcpDepartments.has(department));
2533
+ if (include.length > 0) {
2534
+ departments.include = uniqueStrings(include);
2535
+ }
2536
+ else {
2537
+ delete departments.include;
2538
+ }
2539
+ if (exclude.length > 0) {
2540
+ departments.exclude = uniqueStrings(exclude);
2541
+ }
2542
+ else {
2543
+ delete departments.exclude;
2544
+ }
2545
+ if (departments.include === undefined && departments.exclude === undefined) {
2546
+ delete icp.departments;
2547
+ }
2548
+ }
2549
+ function normalizeMcpCompanyIcpMarkets(input) {
2550
+ const markets = normalizeStringArray(input);
2551
+ return uniqueStrings(markets.flatMap(expandMcpCompanyIcpMarket));
2552
+ }
2553
+ function expandMcpCompanyIcpMarket(market) {
2554
+ const trimmed = market.trim();
2555
+ if (!trimmed) {
2556
+ return [];
2557
+ }
2558
+ return prospeoCompanyIcpRegionAliases.get(trimmed.toLowerCase()) ?? [trimmed];
2559
+ }
2560
+ function stripMcpDuplicateCompanyIndustry(filters) {
2561
+ const icp = filters.company_icp;
2562
+ if (isPlainObject(icp) &&
2563
+ Array.isArray(icp.industries) &&
2564
+ icp.industries.length > 0) {
2565
+ delete filters.company_industry;
2566
+ }
2567
+ }
2568
+ function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds) {
2569
+ if (!hasSeeds || !isPlainObject(filters.company_lookalike)) {
2570
+ return;
2571
+ }
2572
+ const icp = filters.company_icp;
2573
+ if (isPlainObject(icp) &&
2574
+ Array.isArray(icp.industries) &&
2575
+ icp.industries.length > 0 &&
2576
+ !filters.company_industry) {
2577
+ filters.company_industry = { include: icp.industries };
2578
+ }
2579
+ delete filters.company_icp;
2580
+ delete filters.company_keywords;
2581
+ delete filters.company_website_search;
2582
+ }
2583
+ function stripMcpKeyCustomerCompanionFilters(filters) {
2584
+ const keyCustomers = filters.company_key_customers;
2585
+ if (!isPlainObject(keyCustomers)) {
2586
+ return;
2587
+ }
2588
+ const include = normalizeStringArray(keyCustomers.include);
2589
+ if (include.length === 0) {
2590
+ return;
2591
+ }
2592
+ for (const filterName of prospeoKeyCustomerCompanionFilters) {
2593
+ delete filters[filterName];
2594
+ }
2595
+ }
2596
+ function normalizeMcpPersonJobTitle(filters) {
2597
+ const jobTitle = filters.person_job_title;
2598
+ if (!isPlainObject(jobTitle)) {
2599
+ return;
2600
+ }
2601
+ const include = normalizeStringArray(jobTitle.include);
2602
+ const exclude = normalizeStringArray(jobTitle.exclude);
2603
+ if (include.length > PROSPEO_LARGE_TITLE_INCLUDE_BOOLEAN_THRESHOLD &&
2604
+ exclude.length === 0 &&
2605
+ typeof jobTitle.boolean_search !== "string") {
2606
+ delete jobTitle.include;
2607
+ jobTitle.boolean_search = include
2608
+ .map((title) => `"${title.replace(/"/g, '\\"')}"`)
2609
+ .join(" OR ");
2610
+ }
2611
+ }
2612
+ function isPlainObject(input) {
2613
+ return Boolean(input) && typeof input === "object" && !Array.isArray(input);
2614
+ }
2615
+ function normalizeStringArray(input) {
2616
+ if (!Array.isArray(input)) {
2617
+ return [];
2618
+ }
2619
+ return input
2620
+ .filter((value) => typeof value === "string")
2621
+ .map((value) => value.trim())
2622
+ .filter((value) => value.length > 0);
2623
+ }
2624
+ function normalizeMcpSeeds(input, kind) {
2625
+ return uniqueStrings(normalizeStringArray(input).filter((seed) => kind === "domain" ? isLikelyConcreteDomain(seed) : isLikelyConcreteSeedCompany(seed)));
2626
+ }
2627
+ function isLikelyConcreteDomain(input) {
2628
+ return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(input);
2629
+ }
2630
+ function isLikelyConcreteSeedCompany(input) {
2631
+ const normalized = input.toLowerCase();
2632
+ return !(normalized.includes("placeholder") ||
2633
+ normalized.includes("another approved") ||
2634
+ normalized.includes("approved seed") ||
2635
+ normalized.includes("best-customer seed") ||
2636
+ normalized.includes("example seed"));
2637
+ }
2638
+ function uniqueStrings(input) {
2639
+ return Array.from(new Set(input));
2640
+ }
2641
+ function removeEmptyObjectFilter(filters, filterName) {
2642
+ const filter = filters[filterName];
2643
+ if (isPlainObject(filter) && Object.keys(filter).length === 0) {
2644
+ delete filters[filterName];
2645
+ }
2646
+ }
2385
2647
  function clonePlainObject(input) {
2386
2648
  return JSON.parse(JSON.stringify(input));
2387
2649
  }
@@ -2514,7 +2776,8 @@ export async function searchProspeo(input) {
2514
2776
  nestedCompany?.names !== undefined) {
2515
2777
  throw new Error("search_prospeo does not accept filters.company.websites or filters.company.names. For known accounts, resolve names/domains into a domainFilterId with load_csv_domains or save_domain_filters. For company/account lookalikes, use search_prospeo_companies first, then confirm_prospeo_company_accounts.");
2516
2778
  }
2517
- const response = await api.post(`/api/v3/prospeo/search`, input);
2779
+ const safeInput = normalizeProspeoSearchInputForMcp(input);
2780
+ const response = await api.post(`/api/v3/prospeo/search`, safeInput);
2518
2781
  return compactProspeoSearchResponse(response);
2519
2782
  }
2520
2783
  function compactProspeoSearchResponse(response) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -219,7 +219,6 @@ Package-backed MCP may return a short `mcp-prospeo-company-search-token:*`
219
219
  reference to avoid long-token copy errors. Account rows are not people leads
220
220
  yet. The confirmation creates the `domainFilterId` that constrains the follow-on
221
221
  `search_prospeo` people search.
222
- Always copy the `companySearchToken` exactly.
223
222
 
224
223
  Prospeo company/account search is useful when the source plan depends on
225
224
  website traffic (`company_website_traffic`), confirmed AI Attributes including
@@ -253,6 +252,12 @@ without an include keyword, and do not duplicate `company_industry` when
253
252
  search, prefer `person_job_title.boolean_search` for long role synonym lists
254
253
  instead of many `person_job_title.include` values plus broad department/seniority
255
254
  filters.
255
+ For seeded company lookalikes, keep the first call simple: resolved seed
256
+ company/domain plus `company_lookalike.minimum_tier` and simple confirmed
257
+ attributes, headcount, or industry. Do not add `company_website_search`,
258
+ `company_keywords`, or `company_icp` until the account sample proves the seed
259
+ works. Do not send placeholder seed names like `another approved best-customer seed`,
260
+ and only use concrete companies or domains you actually resolved. Do not send `company_website_search.exclude_keywords` without a positive website include signal.
256
261
  Do not use `AI`, `API`, `GTM`, or `SaaS` as company keyword terms.
257
262
  Do not combine `company_key_customers` with ICP, website-search, keyword,
258
263
  attribute, industry, or headcount filters until the standalone pass proves
@@ -427,6 +427,12 @@ Use first for broad persona expansion, ABM/domain targeting, hiring-led targetin
427
427
  - Do not send `company_keywords.exclude` unless at least one include keyword is
428
428
  present. Do not duplicate `company_industry` when `company_icp.industries`
429
429
  already carries the industry.
430
+ - For seeded company lookalikes, keep the first call simple: resolved seed
431
+ company/domain plus `company_lookalike.minimum_tier` and simple confirmed
432
+ attributes, headcount, or industry. Do not add `company_website_search`,
433
+ `company_keywords`, or `company_icp` until the account sample proves the seed
434
+ works. Do not send placeholder seed names like `another approved best-customer seed`,
435
+ and only use concrete companies or domains you actually resolved. Do not send `company_website_search.exclude_keywords` without a positive website include signal.
430
436
  - Do not use `company_intent`. Do not invent unsupported support-channel filters
431
437
  or AI Attribute guesses like phone/email/chat/ticket/social.
432
438
  - Company/account search returns an account sample only; account rows are not people leads yet. Ask the user to approve the account sample.
@@ -106,6 +106,9 @@ Avoidable-400 guardrails:
106
106
  - Do not use `AI`, `API`, `GTM`, or `SaaS` as company keyword terms. Use confirmed attributes such as `uses_ai` / `has_api`, or spell out `artificial intelligence`, `application programming interface`, `go to market`, and `software as a service`.
107
107
  - Do not send `company_keywords.exclude` unless at least one include keyword is present.
108
108
  - Do not duplicate `company_industry` when `company_icp.industries` already carries the industry.
109
+ - For seeded company lookalikes, keep the first call simple: seed company/domain + `company_lookalike.minimum_tier` + confirmed attributes, headcount, or industry. Do not add `company_website_search`, `company_keywords`, or `company_icp` until the account sample proves the seed works.
110
+ - Do not send placeholder seed names such as `another approved best-customer seed`; use only concrete company names or domains you have actually resolved.
111
+ - Do not send `company_website_search.exclude_keywords` without a positive website include signal.
109
112
  - For post-confirm people search, prefer `person_job_title.boolean_search` for long role synonym lists instead of many `person_job_title.include` values plus broad department/seniority filters.
110
113
 
111
114
  Unsupported/caveats:
@@ -249,6 +252,7 @@ Preference rules:
249
252
  - `company_keywords.include/exclude` values must be at least 3 characters; use `artificial intelligence` instead of `AI`, or use confirmed attributes such as `uses_ai` when that is the real signal.
250
253
  - Do not use `AI`, `API`, `GTM`, or `SaaS` as company keyword terms; use longer phrases such as `artificial intelligence`, `application programming interface`, `go to market`, or `software as a service`.
251
254
  - Run `company_key_customers` as a standalone first-pass filter. Do not combine `company_key_customers` with ICP, website-search, keyword, attribute, industry, or headcount filters until the standalone pass proves useful.
255
+ - For seeded lookalikes, avoid first-call `company_website_search`, `company_keywords`, and `company_icp`; use resolved seeds plus lookalike tier and simple attributes/headcount/industry first.
252
256
 
253
257
  ### Person Filters
254
258