@sellable/mcp 0.1.231 → 0.1.233

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.
@@ -522,6 +522,7 @@ search_prospeo_companies({
522
522
  }
523
523
  })
524
524
  Do not invent company_oids. When seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo company IDs.
525
+ For customer, best-customer, top-customer, or past-company lookalikes, never substitute the sender's current company or domain as a lookalike seed and never use the sender's current company/domain as a silent substitute. Use explicit customer/account/company/domain input first, then verified customer/worked-with/past-company evidence from research/proof, then ask for the missing seed or switch to non-lookalike company filters if YOLO requires moving without a seed. Label profile/work-history seeds as past-company/public proof clues, not confirmed customers unless the source proves customer status. If the user asks for a geography like Germany, preserve it in company discovery where supported and in the follow-on people search.
525
526
  After account approval, copy the companySearchToken exactly into confirm_prospeo_company_accounts; package-backed MCP may return a short mcp-prospeo-company-search-token:* reference to avoid long-token copy errors.
526
527
  For accounts in the news: { "company_news": { "categories": ["Funding & Investment"], "timeframe_days": 90 } }.
527
528
  For awards: { "company_awards": { "include": ["G2"], "match_mode": "CONTAINS" } }.
@@ -1497,7 +1498,8 @@ export const leadToolDefinitions = [
1497
1498
  },
1498
1499
  {
1499
1500
  name: "search_prospeo_companies",
1500
- description: 'Search Prospeo for company/account results through the public /search-company lane. Requires get_provider_prompt({ provider: "prospeo" }) first. Use this first for company lookalike/account asks such as "find companies like Red Rover that use AI and have an API", "find founders at companies like our best customers", or "find accounts in the news about funding or partnerships". Results are accounts, not finished people leads. Review the returned accounts with the user, then call confirm_prospeo_company_accounts with the companySearchToken to create a domainFilterId before using search_prospeo for people at those accounts. Supports company lookalike, confirmed AI Attributes including pricing, company news, awards, website search, products/services, integrations, key customers, operating languages, Google discovery, headcount by location, structured company ICP, and experimental website traffic/key executive filters. Do not invent company_oids; when seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo IDs. company_intent and support-channel guesses like phone/email/chat/ticket/social are unsupported. Public API rows do not include lookalike tier/score/reason. ' +
1501
+ description: 'Search Prospeo for company/account results through the public /search-company lane. Requires get_provider_prompt({ provider: "prospeo" }) first. Use this first for company lookalike/account asks such as "find companies like Red Rover that use AI and have an API", "find founders at companies like our best customers", or "find accounts in the news about funding or partnerships". Results are accounts, not finished people leads. Review the returned accounts with the user, then call confirm_prospeo_company_accounts with the companySearchToken to create a domainFilterId before using search_prospeo for people at those accounts. Supports company lookalike, confirmed AI Attributes including pricing, company news, awards, website search, products/services, integrations, key customers, operating languages, Google discovery, headcount by location, structured company ICP, and experimental website traffic/key executive filters. Do not invent company_oids; when seedCompanies or seedDomains are present, omit company_oids and let the backend resolve real Prospeo IDs. company_intent and support-channel guesses like phone/email/chat/ticket/social are unsupported. Public API rows do not include lookalike tier/score/reason. If output includes omittedFilters or warnings, report them as truthful MCP/backend safety handling rather than claiming Prospeo enforced those omitted constraints. ' +
1502
+ "For best-customer, top-customer, customer, or past-company lookalikes, never substitute the sender's current company/domain as the seed unless current-company peers were explicitly requested; use explicit or verified customer/account/past-company seeds and preserve geography like Germany in account or people filters. " +
1501
1503
  prospeoCompanyAccountWorkflowGuidance,
1502
1504
  inputSchema: {
1503
1505
  type: "object",
@@ -1630,7 +1632,7 @@ export const leadToolDefinitions = [
1630
1632
  },
1631
1633
  {
1632
1634
  name: "search_prospeo",
1633
- description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1635
+ description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. If output includes warnings/fallback, report that the MCP retried a rejected precise title request with a safer domain-filter people search instead of claiming the original precise filter succeeded. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1634
1636
  inputSchema: {
1635
1637
  type: "object",
1636
1638
  properties: {
@@ -2367,7 +2369,7 @@ export async function searchProspeoCompanies(input) {
2367
2369
  page: safeInput?.page,
2368
2370
  sort: safeInput?.sort,
2369
2371
  }));
2370
- return compactProspeoCompanySearchResponse(response);
2372
+ return compactProspeoCompanySearchResponse(response, safeInput.omittedFilters ?? []);
2371
2373
  }
2372
2374
  export async function confirmProspeoCompanyAccounts(input) {
2373
2375
  if (!input?.companySearchToken) {
@@ -2404,6 +2406,7 @@ function removeUndefinedValues(input) {
2404
2406
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
2405
2407
  }
2406
2408
  function normalizeProspeoCompanySearchInputForMcp(input) {
2409
+ const omittedFilters = [];
2407
2410
  const seedDomains = normalizeMcpSeeds(input.seedDomains, "domain");
2408
2411
  const seedCompanies = seedDomains.length > 0
2409
2412
  ? []
@@ -2418,22 +2421,28 @@ function normalizeProspeoCompanySearchInputForMcp(input) {
2418
2421
  typeof lookalike === "object" &&
2419
2422
  !Array.isArray(lookalike) &&
2420
2423
  "company_oids" in lookalike) {
2424
+ omittedFilters.push({
2425
+ field: "company_lookalike.company_oids",
2426
+ reason: "Dropped model-supplied company_oids because seedDomains/seedCompanies require backend resolution of real Prospeo IDs.",
2427
+ value: lookalike.company_oids,
2428
+ });
2421
2429
  delete lookalike.company_oids;
2422
2430
  removeEmptyObjectFilter(filters, "company_lookalike");
2423
2431
  }
2424
2432
  }
2425
- relaxMcpSeedMatchAll(filters, seedDomains.length + seedCompanies.length);
2426
- normalizeMcpCompanyKeywords(filters);
2427
- normalizeMcpCompanyWebsiteSearch(filters);
2433
+ normalizeMcpSeedMatchAll(filters, inferConcreteLookalikeSeedCount(filters, seedDomains.length + seedCompanies.length), omittedFilters);
2434
+ normalizeMcpCompanyKeywords(filters, omittedFilters);
2435
+ normalizeMcpCompanyWebsiteSearch(filters, omittedFilters);
2428
2436
  normalizeMcpCompanyIcp(filters);
2429
2437
  stripMcpDuplicateCompanyIndustry(filters);
2430
- stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds || hasMcpCompanyLookalikeOids(filters));
2438
+ stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds || hasMcpCompanyLookalikeOids(filters), omittedFilters);
2431
2439
  stripMcpKeyCustomerCompanionFilters(filters);
2432
2440
  return {
2433
2441
  ...input,
2434
2442
  seedCompanies: seedCompanies.length > 0 ? seedCompanies : undefined,
2435
2443
  seedDomains: seedDomains.length > 0 ? seedDomains : undefined,
2436
2444
  filters,
2445
+ omittedFilters: omittedFilters.length > 0 ? omittedFilters : undefined,
2437
2446
  };
2438
2447
  }
2439
2448
  function normalizeProspeoSearchInputForMcp(input) {
@@ -2446,7 +2455,7 @@ function normalizeProspeoSearchInputForMcp(input) {
2446
2455
  filters,
2447
2456
  };
2448
2457
  }
2449
- function normalizeMcpCompanyKeywords(filters) {
2458
+ function normalizeMcpCompanyKeywords(filters, omittedFilters = []) {
2450
2459
  const keywords = filters.company_keywords;
2451
2460
  if (!isPlainObject(keywords)) {
2452
2461
  return;
@@ -2460,6 +2469,13 @@ function normalizeMcpCompanyKeywords(filters) {
2460
2469
  keywords.include = uniqueStrings(validInclude);
2461
2470
  }
2462
2471
  else {
2472
+ if (keywords.exclude !== undefined) {
2473
+ omittedFilters.push({
2474
+ field: "company_keywords.exclude",
2475
+ reason: "Dropped exclude-only company_keywords because Prospeo company search requires a positive keyword include signal.",
2476
+ value: keywords.exclude,
2477
+ });
2478
+ }
2463
2479
  delete keywords.include;
2464
2480
  delete keywords.exclude;
2465
2481
  }
@@ -2468,7 +2484,7 @@ function normalizeMcpCompanyKeywords(filters) {
2468
2484
  }
2469
2485
  removeEmptyObjectFilter(filters, "company_keywords");
2470
2486
  }
2471
- function normalizeMcpCompanyWebsiteSearch(filters) {
2487
+ function normalizeMcpCompanyWebsiteSearch(filters, omittedFilters = []) {
2472
2488
  const websiteSearch = filters.company_website_search;
2473
2489
  if (!isPlainObject(websiteSearch)) {
2474
2490
  return;
@@ -2479,6 +2495,13 @@ function normalizeMcpCompanyWebsiteSearch(filters) {
2479
2495
  normalizeStringArray(websiteSearch.urls).length > 0 ||
2480
2496
  Object.entries(websiteSearch).some(([key, value]) => key.startsWith("has_") && typeof value === "boolean" && value);
2481
2497
  if (!hasPositiveWebsiteSignal) {
2498
+ if (websiteSearch.exclude_keywords !== undefined) {
2499
+ omittedFilters.push({
2500
+ field: "company_website_search.exclude_keywords",
2501
+ reason: "Dropped website-search exclusions because Prospeo needs a positive website signal before exclude_keywords are safe.",
2502
+ value: websiteSearch.exclude_keywords,
2503
+ });
2504
+ }
2482
2505
  delete websiteSearch.exclude_keywords;
2483
2506
  }
2484
2507
  removeEmptyObjectFilter(filters, "company_website_search");
@@ -2568,11 +2591,27 @@ function stripMcpDuplicateCompanyIndustry(filters) {
2568
2591
  delete filters.company_industry;
2569
2592
  }
2570
2593
  }
2571
- function relaxMcpSeedMatchAll(filters, seedCount) {
2572
- if (seedCount <= 1 || !isPlainObject(filters.company_lookalike)) {
2594
+ function inferConcreteLookalikeSeedCount(filters, explicitSeedCount) {
2595
+ if (explicitSeedCount > 0) {
2596
+ return explicitSeedCount;
2597
+ }
2598
+ const lookalike = filters.company_lookalike;
2599
+ if (!isPlainObject(lookalike) || !Array.isArray(lookalike.company_oids)) {
2600
+ return 0;
2601
+ }
2602
+ return lookalike.company_oids.filter((oid) => typeof oid === "string" && /^cccc[a-z0-9]+$/i.test(oid)).length;
2603
+ }
2604
+ function normalizeMcpSeedMatchAll(filters, concreteSeedCount, omittedFilters = []) {
2605
+ if (!isPlainObject(filters.company_lookalike)) {
2573
2606
  return;
2574
2607
  }
2575
- if (filters.company_lookalike.match_all === true) {
2608
+ if (filters.company_lookalike.match_all === true &&
2609
+ concreteSeedCount < 2) {
2610
+ omittedFilters.push({
2611
+ field: "company_lookalike.match_all",
2612
+ reason: "Dropped match_all because fewer than two concrete approved lookalike seeds remained after MCP seed normalization.",
2613
+ value: true,
2614
+ });
2576
2615
  delete filters.company_lookalike.match_all;
2577
2616
  }
2578
2617
  }
@@ -2582,7 +2621,7 @@ function hasMcpCompanyLookalikeOids(filters) {
2582
2621
  Array.isArray(lookalike.company_oids) &&
2583
2622
  lookalike.company_oids.some((oid) => typeof oid === "string" && oid.length > 0));
2584
2623
  }
2585
- function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds) {
2624
+ function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds, omittedFilters = []) {
2586
2625
  if (!hasSeeds || !isPlainObject(filters.company_lookalike)) {
2587
2626
  return;
2588
2627
  }
@@ -2593,17 +2632,43 @@ function stripMcpSeededLookalikeRiskyRefinements(filters, hasSeeds) {
2593
2632
  !filters.company_industry) {
2594
2633
  filters.company_industry = { include: icp.industries };
2595
2634
  }
2596
- delete filters.company_icp;
2597
- delete filters.company_keywords;
2598
- delete filters.company_website_search;
2599
- simplifyMcpSeededLookalikeAttributes(filters);
2635
+ if (filters.company_icp !== undefined) {
2636
+ omittedFilters.push({
2637
+ field: "company_icp",
2638
+ reason: "Dropped company_icp from seeded lookalike account search; use it as a later refinement after the seed returns a safe account sample.",
2639
+ value: filters.company_icp,
2640
+ });
2641
+ delete filters.company_icp;
2642
+ }
2643
+ if (filters.company_keywords !== undefined) {
2644
+ omittedFilters.push({
2645
+ field: "company_keywords",
2646
+ reason: "Dropped company_keywords from seeded lookalike account search to avoid Prospeo vendor 400s; use keywords as a later refinement after the seed works.",
2647
+ value: filters.company_keywords,
2648
+ });
2649
+ delete filters.company_keywords;
2650
+ }
2651
+ if (filters.company_website_search !== undefined) {
2652
+ omittedFilters.push({
2653
+ field: "company_website_search",
2654
+ reason: "Dropped company_website_search from seeded lookalike account search to avoid Prospeo vendor 400s; use website search as a later refinement after the seed works.",
2655
+ value: filters.company_website_search,
2656
+ });
2657
+ delete filters.company_website_search;
2658
+ }
2659
+ simplifyMcpSeededLookalikeAttributes(filters, omittedFilters);
2600
2660
  }
2601
- function simplifyMcpSeededLookalikeAttributes(filters) {
2661
+ function simplifyMcpSeededLookalikeAttributes(filters, omittedFilters = []) {
2602
2662
  const attributes = filters.company_attributes;
2603
2663
  if (!isPlainObject(attributes)) {
2604
2664
  return;
2605
2665
  }
2606
2666
  if (attributes.has_api === true && attributes.has_sso === true) {
2667
+ omittedFilters.push({
2668
+ field: "company_attributes.has_sso",
2669
+ reason: "Dropped has_sso from the first seeded lookalike call because has_api + has_sso is safer as a two-step refinement.",
2670
+ value: true,
2671
+ });
2607
2672
  delete attributes.has_sso;
2608
2673
  }
2609
2674
  }
@@ -2694,7 +2759,7 @@ function storeProspeoCompanySearchTokenRef(token) {
2694
2759
  function resolveProspeoCompanySearchTokenRef(token) {
2695
2760
  return prospeoCompanySearchTokenRefs.get(token) ?? token;
2696
2761
  }
2697
- function compactProspeoCompanySearchResponse(response) {
2762
+ function compactProspeoCompanySearchResponse(response, omittedFilters = []) {
2698
2763
  if (!response || typeof response !== "object") {
2699
2764
  return response;
2700
2765
  }
@@ -2704,7 +2769,7 @@ function compactProspeoCompanySearchResponse(response) {
2704
2769
  const sampleResults = accountResults
2705
2770
  .slice(0, 10)
2706
2771
  .map(compactProspeoCompanyAccount);
2707
- return {
2772
+ return removeUndefinedValues({
2708
2773
  success: response.success ?? true,
2709
2774
  totalCount: typeof response.totalCount === "number"
2710
2775
  ? response.totalCount
@@ -2723,9 +2788,19 @@ function compactProspeoCompanySearchResponse(response) {
2723
2788
  },
2724
2789
  requestedFilters: response.normalizedFilters ?? response.filters ?? {},
2725
2790
  warnings: Array.isArray(response.warnings) ? response.warnings : [],
2791
+ omittedFilters: omittedFilters.length > 0
2792
+ ? sanitizeMcpFilterAdjustments(omittedFilters)
2793
+ : undefined,
2726
2794
  companySearchToken: storeProspeoCompanySearchTokenRef(response.companySearchToken),
2727
2795
  nextStep: "Review accounts, then call confirm_prospeo_company_accounts with companySearchToken and selectedCompanyIds. These accounts are not people leads yet.",
2728
- };
2796
+ });
2797
+ }
2798
+ function sanitizeMcpFilterAdjustments(adjustments) {
2799
+ return adjustments.map((adjustment) => removeUndefinedValues({
2800
+ field: adjustment.field,
2801
+ reason: adjustment.reason,
2802
+ value: adjustment.value,
2803
+ }));
2729
2804
  }
2730
2805
  function compactProspeoCompanyAccount(account) {
2731
2806
  const compact = {
@@ -2804,10 +2879,99 @@ export async function searchProspeo(input) {
2804
2879
  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.");
2805
2880
  }
2806
2881
  const safeInput = normalizeProspeoSearchInputForMcp(input);
2807
- const response = await api.post(`/api/v3/prospeo/search`, safeInput);
2808
- return compactProspeoSearchResponse(response);
2882
+ try {
2883
+ const response = await api.post(`/api/v3/prospeo/search`, safeInput);
2884
+ return compactProspeoSearchResponse(response);
2885
+ }
2886
+ catch (error) {
2887
+ const fallbackInput = buildProspeoPeopleSearchFallbackInput(safeInput, error);
2888
+ if (!fallbackInput) {
2889
+ throw error;
2890
+ }
2891
+ const fallbackResponse = await api.post(`/api/v3/prospeo/search`, fallbackInput);
2892
+ const keyword = String(fallbackInput.filters.person_name_or_job_title ?? "title keyword");
2893
+ return compactProspeoSearchResponse(fallbackResponse, {
2894
+ warnings: [
2895
+ `Prospeo rejected precise person-title filters for this domainFilterId; retried with person_name_or_job_title "${keyword}" plus seniority fallback.`,
2896
+ ],
2897
+ fallback: {
2898
+ reason: "precise_person_title_filters_rejected",
2899
+ originalStatus: getErrorStatus(error),
2900
+ filters: fallbackInput.filters,
2901
+ },
2902
+ });
2903
+ }
2904
+ }
2905
+ function getErrorStatus(error) {
2906
+ if (!error || typeof error !== "object") {
2907
+ return undefined;
2908
+ }
2909
+ const status = error.status;
2910
+ return typeof status === "number" ? status : undefined;
2911
+ }
2912
+ function buildProspeoPeopleSearchFallbackInput(input, error) {
2913
+ if (getErrorStatus(error) !== 400 || !input.domainFilterId) {
2914
+ return null;
2915
+ }
2916
+ const filters = input.filters;
2917
+ const jobTitle = filters?.person_job_title;
2918
+ if (!isPlainObject(jobTitle) || filters.person_name_or_job_title) {
2919
+ return null;
2920
+ }
2921
+ const keyword = inferProspeoPeopleFallbackKeyword(jobTitle);
2922
+ if (!keyword) {
2923
+ return null;
2924
+ }
2925
+ const fallbackFilters = {
2926
+ person_name_or_job_title: keyword,
2927
+ person_seniority: {
2928
+ include: [
2929
+ "C-Suite",
2930
+ "Vice President",
2931
+ "Head",
2932
+ "Director",
2933
+ "Manager",
2934
+ ],
2935
+ },
2936
+ max_person_per_company: typeof filters.max_person_per_company === "number"
2937
+ ? filters.max_person_per_company
2938
+ : 1,
2939
+ };
2940
+ if (isPlainObject(filters.person_location_search)) {
2941
+ fallbackFilters.person_location_search = filters.person_location_search;
2942
+ }
2943
+ return removeUndefinedValues({
2944
+ campaignOfferId: input.campaignOfferId,
2945
+ domainFilterId: input.domainFilterId,
2946
+ page: input.page,
2947
+ searchName: input.searchName,
2948
+ currentStep: input.currentStep,
2949
+ confirmed: input.confirmed,
2950
+ filters: fallbackFilters,
2951
+ });
2952
+ }
2953
+ function inferProspeoPeopleFallbackKeyword(jobTitle) {
2954
+ const text = [
2955
+ typeof jobTitle.boolean_search === "string" ? jobTitle.boolean_search : "",
2956
+ ...normalizeStringArray(jobTitle.include),
2957
+ ].join(" ");
2958
+ if (/security|ciso|appsec|soc/i.test(text))
2959
+ return "Security";
2960
+ if (/platform/i.test(text))
2961
+ return "Platform";
2962
+ if (/product/i.test(text))
2963
+ return "Product";
2964
+ if (/growth|gtm|go[-\s]?to[-\s]?market/i.test(text))
2965
+ return "Growth";
2966
+ if (/sales|revenue/i.test(text))
2967
+ return "Sales";
2968
+ if (/marketing|demand/i.test(text))
2969
+ return "Marketing";
2970
+ if (/engineering|technical|developer/i.test(text))
2971
+ return "Engineering";
2972
+ return null;
2809
2973
  }
2810
- function compactProspeoSearchResponse(response) {
2974
+ function compactProspeoSearchResponse(response, metadata = {}) {
2811
2975
  if (!response || typeof response !== "object") {
2812
2976
  return response;
2813
2977
  }
@@ -2815,7 +2979,7 @@ function compactProspeoSearchResponse(response) {
2815
2979
  ? response.people.results
2816
2980
  : [];
2817
2981
  const sampleResults = peopleResults.slice(0, 10).map(compactProspeoPerson);
2818
- return {
2982
+ return removeUndefinedValues({
2819
2983
  success: response.success ?? true,
2820
2984
  searchId: response.searchId ?? null,
2821
2985
  searchName: response.searchName ?? null,
@@ -2836,7 +3000,9 @@ function compactProspeoSearchResponse(response) {
2836
3000
  sampleCount: sampleResults.length,
2837
3001
  results: sampleResults,
2838
3002
  },
2839
- };
3003
+ warnings: metadata.warnings,
3004
+ fallback: metadata.fallback,
3005
+ });
2840
3006
  }
2841
3007
  function compactProspeoPerson(person) {
2842
3008
  const currentRole = Array.isArray(person?._prospeo?.job_history)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.231",
3
+ "version": "0.1.233",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -220,6 +220,23 @@ 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
222
 
223
+ For lookalike seed selection, treat "best customer", "top customer", "past
224
+ companies", "companies I worked with", and similar wording as customer/account
225
+ or past-company seed asks. Never substitute the sender's current company or
226
+ domain as a lookalike seed; never use the sender's current company/domain as a
227
+ silent substitute unless the user explicitly asks for companies like the current
228
+ employer, sender company, or its direct peers/competitors. Seed priority is:
229
+ explicit user-provided customer/account/company/domain; verified
230
+ customer/worked-with/past-company evidence from research/proof; then ask for the
231
+ missing seed or switch to non-lookalike company filters if YOLO requires moving
232
+ without a seed. If using profile/work-history seeds, label them as
233
+ past-company/public proof clues, not confirmed customers unless the source proves
234
+ customer status. If the user asks for a geography like Germany, preserve it in
235
+ account discovery where supported (`company_location_search` or
236
+ `company_icp.geographic_markets`) and in follow-on people search
237
+ (`person_location_search`); do not drop geography when moving from lookalike
238
+ accounts to people leads.
239
+
223
240
  Prospeo company/account search is useful when the source plan depends on
224
241
  website traffic (`company_website_traffic`), confirmed AI Attributes including
225
242
  `pricing`, `uses_ai`, `has_api`, `has_chrome_extension`, `has_sso`,
@@ -24,13 +24,13 @@
24
24
  "useWhen": [
25
25
  "You want Prospeo filters or domain-based search",
26
26
  "You need company lookalike account discovery before finding people",
27
- "You want companies like X, best-customer lookalikes, or accounts using AI/API/SSO/Chrome extensions",
27
+ "You want companies like X, best-customer/top-customer/past-company lookalikes, or accounts using AI/API/SSO/Chrome extensions",
28
28
  "You need companies hiring for specific roles using job-posting filters",
29
29
  "You need high deliverability from Prospeo"
30
30
  ],
31
31
  "avoidWhen": ["You need LinkedIn activity filters"],
32
32
  "reason": "Strong for hiring-led search, company/account lookalikes, Prospeo-specific filters, verified contacts, and domain lists.",
33
- "prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo can discover lookalike accounts first, then turn approved accounts into a domainFilterId for people search\n- Prospeo can filter for companies hiring specific roles with job-posting filters\n- We can pair those company signals with buyer/referrer titles and verified-contact coverage\n\nFor lookalike accounts, I will show an account sample first; those account rows are not people leads yet. After approval, I will use the companySearchToken to confirm the accounts, then search people at the returned domainFilterId.\n\nShould I search Prospeo for {icp}?"
33
+ "prose": "Since you're targeting **{icp}**, I'd recommend **Prospeo**.\n\nHere's why:\n- Prospeo can discover lookalike accounts first, then turn approved accounts into a domainFilterId for people search\n- Prospeo can filter for companies hiring specific roles with job-posting filters\n- We can pair those company signals with buyer/referrer titles and verified-contact coverage\n\nFor lookalike accounts, I will show an account sample first; those account rows are not people leads yet. After approval, I will use the companySearchToken to confirm the accounts, then search people at the returned domainFilterId. For best-customer, top-customer, or past-company lookalikes, I will use explicit customer/account/past-company seeds, not the sender's current company, unless you explicitly ask for current-company peers.\n\nShould I search Prospeo for {icp}?"
34
34
  },
35
35
  "askOption": {
36
36
  "label": "Prospeo",
@@ -99,7 +99,7 @@ Is your ICP LinkedIn-active (founders, sales, marketing, GTM, engineers)?
99
99
  Need hiring-by-role filters? -> Prospeo (has company job-posting filters)
100
100
  Need tech stack targeting? -> Apollo (has technology filters)
101
101
  Have specific company names/domains? -> Sales Navigator for small lists or Prospeo with domainFilterId
102
- Need companies like X, best-customer lookalikes, AI/API/SSO/Chrome extension filters, news/awards/integrations/key customers? -> Prospeo account discovery first
102
+ Need companies like X, best-customer/top-customer/past-company lookalikes, AI/API/SSO/Chrome extension filters, news/awards/integrations/key customers? -> Prospeo account discovery first; use explicit customer/account/past-company seeds, not the sender's current company unless current-company peers were requested
103
103
  ```
104
104
 
105
105
  ## ICP-to-Provider Quick Reference
@@ -188,6 +188,23 @@ Show the account sample first, ask approval, then pass the returned
188
188
  `confirm_prospeo_company_accounts`. The confirmed `domainFilterId` constrains
189
189
  the follow-on people search; account rows are not people leads yet.
190
190
 
191
+ For lookalike seed selection, treat "best customer", "top customer", "past
192
+ companies", "companies I worked with", and similar wording as customer/account
193
+ or past-company seed asks. Never substitute the sender's current company or
194
+ domain as a lookalike seed; never use the sender's current company/domain as a
195
+ silent substitute unless the user explicitly asks for companies like the current
196
+ employer, sender company, or its direct peers/competitors. Seed priority is:
197
+ explicit user-provided customer/account/company/domain; verified
198
+ customer/worked-with/past-company evidence from research/proof; then ask for the
199
+ missing seed or switch to non-lookalike company filters if YOLO requires moving
200
+ without a seed. If using profile/work-history seeds, label them as
201
+ past-company/public proof clues, not confirmed customers unless the source proves
202
+ customer status. If the user asks for a geography like Germany, preserve it in
203
+ account discovery where supported (`company_location_search` or
204
+ `company_icp.geographic_markets`) and in follow-on people search
205
+ (`person_location_search`); do not drop geography when moving from lookalike
206
+ accounts to people leads.
207
+
191
208
  Before any provider prompt/search/scout call, move the watched campaign to
192
209
  source selection, show `## Find Buyers Plan`, then open `request_user_input`
193
210
  without repeating the URL. The plan must appear before the question and only
@@ -391,6 +391,22 @@ Use first for broad persona expansion, ABM/domain targeting, hiring-led targetin
391
391
  - For companies like X, our best customers, lookalike accounts, companies that use AI, companies with API/SSO/Chrome extension, or
392
392
  news/award/integration/key-customer account filters, use
393
393
  `search_prospeo_companies` before people search.
394
+ - For lookalike seed selection, treat "best customer", "top customer", "past
395
+ companies", "companies I worked with", and similar wording as
396
+ customer/account or past-company seed asks. Never substitute the sender's
397
+ current company or domain as a lookalike seed; never use the sender's current
398
+ company/domain as a silent substitute unless the user explicitly asks for
399
+ companies like the current employer, sender company, or its direct
400
+ peers/competitors. Seed priority is: explicit user-provided
401
+ customer/account/company/domain; verified customer/worked-with/past-company
402
+ evidence from research/proof; then ask for the missing seed or switch to
403
+ non-lookalike company filters if YOLO requires moving without a seed. If using
404
+ profile/work-history seeds, label them as past-company/public proof clues, not
405
+ confirmed customers unless the source proves customer status. If the user asks
406
+ for a geography like Germany, preserve it in account discovery where supported
407
+ (`company_location_search` or `company_icp.geographic_markets`) and in
408
+ follow-on people search (`person_location_search`); do not drop geography when
409
+ moving from lookalike accounts to people leads.
394
410
  - Use Prospeo company/account search when the ask depends on website traffic
395
411
  (`company_website_traffic`), confirmed AI Attributes including `pricing`,
396
412
  `uses_ai`, `has_api`, `has_chrome_extension`, `has_sso`, `has_open_source`,
@@ -87,6 +87,23 @@ backend resolves real Prospeo company IDs. Do not invent company_oids such as
87
87
  placeholder IDs from examples. Only send `company_lookalike.company_oids` when
88
88
  they are real Prospeo company IDs returned by a prior tool/search.
89
89
 
90
+ For lookalike seed selection, treat "best customer", "top customer", "past
91
+ companies", "companies I worked with", and similar wording as customer/account
92
+ or past-company seed asks. Never substitute the sender's current company/domain
93
+ as a lookalike seed, and never use the sender's current company/domain as a
94
+ silent substitute unless the user explicitly asks for companies like the current
95
+ employer, sender company, or its direct peers/competitors. Seed priority is:
96
+ explicit user-provided customer/account/company/domain; verified
97
+ customer/worked-with/past-company evidence from research/proof; then ask for the
98
+ missing seed or switch to non-lookalike company filters if YOLO requires moving
99
+ without a seed. If using profile/work-history seeds, label them as
100
+ past-company/public proof clues, not confirmed customers unless the source proves
101
+ customer status. If the user asks for a geography like Germany, preserve it in
102
+ account discovery where supported (`company_location_search` or
103
+ `company_icp.geographic_markets`) and in follow-on people search
104
+ (`person_location_search`); do not drop geography when moving from lookalike
105
+ accounts to people leads.
106
+
90
107
  For structured ICP sizing, pair `company_icp.company_sizes` with
91
108
  `company_headcount_range` when possible. The MCP normalizer derives headcount
92
109
  ranges for `micro`, `smb`, `midmarket`, `enterprise`, and `large_enterprise`
@@ -109,15 +126,30 @@ Avoidable-400 guardrails:
109
126
  - 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
127
  - For seeded company lookalikes, do not combine `has_api` and `has_sso` in the first call; start with `has_api` and refine after a valid account sample if SSO still matters.
111
128
  - Do not send placeholder seed names such as `another approved best-customer seed`; use only concrete company names or domains you have actually resolved.
129
+ - Do not silently use the sender company as a customer lookalike seed; use
130
+ explicit customer/account/past-company evidence instead.
112
131
  - If the user references another approved seed but does not name it, ask for the
113
132
  seed or run a one-seed lookalike without `match_all`; do not invent a second
114
133
  seed from examples, competitors, or exclusions.
115
134
  - Prefer seed domains for single-seed lookalikes. For multi-seed `match_all`
116
- lookalikes, use concrete company names unless you already know the exact
117
- canonical Prospeo domains; guessed product or marketing domains can 400 with
118
- `match_all`. Do not mix `seedDomains` and `seedCompanies` in the same call.
135
+ lookalikes, use two or more concrete approved seed domains or company names;
136
+ never infer the second seed from examples, competitors, or exclusions. If
137
+ Prospeo rejects `match_all`, the backend may retry without `match_all` and
138
+ return a warning. Report that as a vendor fallback, not as proof that strict
139
+ `match_all` succeeded. Do not mix `seedDomains` and `seedCompanies` in the
140
+ same call.
119
141
  - Do not send `company_website_search.exclude_keywords` without a positive website include signal.
142
+ - If `search_prospeo_companies` returns `omittedFilters`, those fields were
143
+ intentionally not sent to Prospeo to avoid known public-API failures. Report
144
+ them as safety handling and keep the account sample separate from people
145
+ leads; use a follow-up refinement or manual review for those omitted
146
+ constraints instead of claiming Prospeo enforced them.
120
147
  - 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.
148
+ - If `search_prospeo` returns a `warnings` array or `fallback` object after a
149
+ domain-filter people search, report the retry honestly. The MCP may retry a
150
+ Prospeo 400 from precise title filters with `person_name_or_job_title` plus
151
+ seniority so the run produces a usable sample without hiding the vendor
152
+ limitation.
121
153
 
122
154
  Unsupported/caveats:
123
155
 
@@ -262,6 +294,13 @@ Preference rules:
262
294
  - 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.
263
295
  - 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.
264
296
  - For seeded lookalikes, avoid first-call `has_api` + `has_sso`; use one confirmed attribute first, then refine.
297
+ - For customer, best-customer, top-customer, or past-company lookalikes, never
298
+ use the sender's current company/domain as a silent substitute; use explicit
299
+ customer/account/past-company evidence, then label profile-derived seeds as
300
+ past-company/public proof clues unless customer status is verified.
301
+ - If the user gives geography like Germany, keep that geography in the company
302
+ discovery or follow-on people filters instead of dropping it after account
303
+ confirmation.
265
304
  - Do not invent missing approved seeds. If a second seed is not named, ask for it
266
305
  or run one seed without `match_all`.
267
306
  - Prefer `seedDomains` for single-seed lookalikes. For multi-seed `match_all`