@coldiq/mcp 0.3.3 → 0.3.5

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-insight.js","sourceRoot":"","sources":["../../src/utils/selection-insight.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/executor.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import { callApi } from './client.js'
2
2
  import { getProviders, getSearchWebProviders } from './registry.js'
3
3
  import type { Capability, ProviderEntry } from './registry.js'
4
+ import { providerDisplayName } from './utils/provider-display.js'
5
+ import { buildSelectionInsight } from './utils/selection-insight.js'
4
6
 
5
7
  export interface ExecutionResult {
6
8
  data: unknown
7
9
  _meta: {
8
10
  provider: string
11
+ /** Clean branded display name for `provider` (never a raw slug or "Apify"). */
12
+ provider_name?: string
13
+ /** Data-quality-framed reason this provider was selected, for the chat surface. */
14
+ selection_insight?: string
15
+ /** Input dimensions that drove routing (e.g. ["tech-stack filter"]), for UI chips. */
16
+ selection_signals?: string[]
9
17
  latencyMs: number
10
18
  matchedFrom?: Record<string, string>
11
19
  /** Net ColdIQ credits charged for this call (parsed from `X-ColdIQ-Credits-Charged`). */
@@ -306,7 +314,21 @@ export async function executeWithFallback(
306
314
 
307
315
  if (hasResult) {
308
316
  log(`${capability} → ${provider.id} ✓ (${result.latencyMs}ms)`)
309
- const meta: ExecutionResult['_meta'] = { provider: provider.id, latencyMs: result.latencyMs }
317
+ const meta: ExecutionResult['_meta'] = {
318
+ provider: provider.id,
319
+ provider_name: providerDisplayName(provider.id),
320
+ latencyMs: result.latencyMs,
321
+ }
322
+ // `errors.length > 0` here means higher-ranked applicable providers returned
323
+ // nothing and we fell through — soften the insight wording accordingly.
324
+ const insight = buildSelectionInsight(capability, provider.id, input, {
325
+ wasFallback: errors.length > 0,
326
+ pinnedByUser: isConstrained,
327
+ })
328
+ if (insight) {
329
+ meta.selection_insight = insight.insight
330
+ if (insight.signals.length > 0) meta.selection_signals = insight.signals
331
+ }
310
332
  if (options?.matchedFrom && Object.keys(options.matchedFrom).length > 0) {
311
333
  meta.matchedFrom = options.matchedFrom
312
334
  }
package/src/registry.ts CHANGED
@@ -69,22 +69,44 @@ function isNonEmptyArray(v: unknown): boolean {
69
69
  return Array.isArray(v) && v.length > 0
70
70
  }
71
71
 
72
+ // TheirStack attaches a per-job `company_object` whose enrichment arrays dominate the
73
+ // payload (e.g. ~1,600-element technology_slugs/technology_names, plus long_description) —
74
+ // tens of KB per job. Drop these heavy fields while keeping company identity (name, domain,
75
+ // industry, employee_count, linkedin_url, founded_year, revenue, …).
76
+ const HEAVY_COMPANY_OBJECT_KEYS = [
77
+ 'technology_slugs',
78
+ 'technology_names',
79
+ 'company_keywords',
80
+ 'company_tags',
81
+ 'keyword_slugs',
82
+ 'long_description',
83
+ ]
84
+
72
85
  /**
73
- * Drop heavy job-description fields from a `{ data: Job[] }` provider response.
74
- * Job descriptions dominate payload size (tens of KB for a handful of jobs) and
75
- * are off by default; callers opt in via `include_description`.
86
+ * Trim a `{ data: Job[] }` provider response for lean payloads: drop job-level
87
+ * `description`/`description_html` (off by default; callers opt in via `include_description`)
88
+ * and slim each job's `company_object` by removing heavy enrichment arrays. Returns a new
89
+ * object; no-op when there's no `data` array.
76
90
  */
77
- function stripJobDescriptions(data: unknown): unknown {
91
+ function trimJobPayload(data: unknown): unknown {
78
92
  const d = data as { data?: unknown[] }
79
93
  if (!Array.isArray(d.data)) return data
80
94
  return {
81
95
  ...(d as Record<string, unknown>),
82
96
  data: d.data.map((job) => {
83
97
  if (!job || typeof job !== 'object') return job
84
- const { description, description_html, ...rest } = job as Record<string, unknown>
98
+ const { description, description_html, company_object, ...rest } = job as Record<string, unknown>
85
99
  void description
86
100
  void description_html
87
- return rest
101
+ if (company_object && typeof company_object === 'object' && !Array.isArray(company_object)) {
102
+ const slim = Object.fromEntries(
103
+ Object.entries(company_object as Record<string, unknown>).filter(
104
+ ([k]) => !HEAVY_COMPANY_OBJECT_KEYS.includes(k),
105
+ ),
106
+ )
107
+ return { ...rest, company_object: slim }
108
+ }
109
+ return company_object === undefined ? rest : { ...rest, company_object }
88
110
  }),
89
111
  }
90
112
  }
@@ -2226,7 +2248,7 @@ const searchJobsProviders: ProviderEntry[] = [
2226
2248
  // TheirStack returns full job descriptions in a passthrough response (tens of KB).
2227
2249
  // career-site/LinkedIn gate descriptions upstream via descriptionType; TheirStack has
2228
2250
  // no such flag, so strip them here unless the caller explicitly opts in.
2229
- postFilter: (data, input) => (input.include_description === true ? data : stripJobDescriptions(data)),
2251
+ postFilter: (data, input) => (input.include_description === true ? data : trimJobPayload(data)),
2230
2252
  },
2231
2253
  ]
2232
2254
 
@@ -3256,7 +3278,7 @@ const findSignalsProviders: ProviderEntry[] = [
3256
3278
  }),
3257
3279
  hasResult: (data) => isNonEmptyArray((data as { data?: unknown[] }).data),
3258
3280
  // Hiring signals don't need full descriptions; keep payloads lean.
3259
- postFilter: (data) => stripJobDescriptions(data),
3281
+ postFilter: (data) => trimJobPayload(data),
3260
3282
  },
3261
3283
  {
3262
3284
  id: 'signalbase-hiring',
@@ -2,6 +2,8 @@ import { z } from 'zod'
2
2
  import { callApi, type ApiResponse } from '../client.js'
3
3
  import { executeWithFallback, isExecutionError } from '../executor.js'
4
4
  import { resolvePreferredProviders, buildAllFailedError, FIND_EMAILS_PROVIDERS } from '../utils/provider-resolver.js'
5
+ import { providerDisplayName } from '../utils/provider-display.js'
6
+ import { buildSelectionInsight } from '../utils/selection-insight.js'
5
7
 
6
8
  // Single-find_email registry providers that the bulk pipeline does NOT already cover.
7
9
  // Used as a fallback waterfall for residual misses after Prospeo + FullEnrich + Findymail + Icypeas.
@@ -462,6 +464,46 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
462
464
  meta.matchedFrom = resolved.matchedFrom
463
465
  }
464
466
 
467
+ // Batch-level selection insight: the per-person waterfall returns a provider per
468
+ // result, so attribute the batch to the provider that resolved the most emails.
469
+ // Prospeo is the primary bulk step (Step 1); anything else means we relied on a
470
+ // fallback step, so soften the wording.
471
+ if (found > 0) {
472
+ const winCounts = new Map<string, number>()
473
+ for (const r of results) {
474
+ if (r.email && r.provider) winCounts.set(r.provider, (winCounts.get(r.provider) ?? 0) + 1)
475
+ }
476
+ // Tie-break deterministically by waterfall rank: scan in FIND_EMAILS_PROVIDERS
477
+ // order and replace only on a strictly greater count, so an equal-count primary
478
+ // (Prospeo, Step 1) is never displaced by a later fallback step — keeps the
479
+ // attribution stable regardless of people-input order.
480
+ let dominant: string | undefined
481
+ let best = 0
482
+ for (const id of FIND_EMAILS_PROVIDERS) {
483
+ const count = winCounts.get(id) ?? 0
484
+ if (count > best) { best = count; dominant = id }
485
+ }
486
+ if (dominant) {
487
+ const insight = buildSelectionInsight('find_emails', dominant, restInput, {
488
+ // Steps 2-4 only run on Prospeo's misses, so a non-Prospeo winner is a
489
+ // genuine fallthrough — unless the caller pinned providers (no Prospeo step),
490
+ // in which case the pinnedByUser branch governs the wording anyway.
491
+ wasFallback: dominant !== 'prospeo' && !isConstrained,
492
+ pinnedByUser: isConstrained,
493
+ })
494
+ if (insight) {
495
+ meta.selection_insight = insight.insight
496
+ if (insight.signals.length > 0) meta.selection_signals = insight.signals
497
+ }
498
+ }
499
+ }
500
+
501
+ // Surface branded provider names per person so users never see a raw slug.
502
+ const brandedResults = results.map((r) => ({
503
+ ...r,
504
+ provider_name: r.provider ? providerDisplayName(r.provider) : null,
505
+ }))
506
+
465
507
  // Surface the silent-failure case the user reported: 200 OK with all-nulls is misleading
466
508
  // when the nulls were caused by provider failures rather than legitimate no-coverage.
467
509
  const isError = batchStatus === 'failed'
@@ -470,7 +512,7 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
470
512
  content: [
471
513
  {
472
514
  type: 'text' as const,
473
- text: JSON.stringify({ data: { results, found, total: people.length }, _meta: meta }),
515
+ text: JSON.stringify({ data: { results: brandedResults, found, total: people.length }, _meta: meta }),
474
516
  },
475
517
  ],
476
518
  ...(isError ? { isError: true } : {}),
@@ -0,0 +1,150 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Provider display names
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Internal provider IDs (registry.ts + the find_emails waterfall) are a mix of
6
+ // clean vendor names ("apollo"), already-branded slugs ("reddit_ads"), and ugly
7
+ // internal slugs ("limadata-work-email"). User-facing surfaces — the selection
8
+ // insight and `_meta.provider_name` — must show a clean branded label instead.
9
+ //
10
+ // Naming the real data product (FullEnrich, TheirStack, …) is intentional: it IS
11
+ // the ColdIQ "we picked the best tool" intelligence on display. The ONLY thing
12
+ // that must never surface is the Apify backend — and since no ID contains the
13
+ // word "Apify", that constraint is satisfied by mapping the Apify-backed scrapers
14
+ // to their branded data-source name (e.g. "Reddit Ads").
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const DISPLAY_NAMES: Record<string, string> = {
18
+ // --- B2B data vendors (shared across capabilities) -----------------------
19
+ companyenrich: 'CompanyEnrich',
20
+ companyenrich_props: 'CompanyEnrich',
21
+ apollo: 'Apollo',
22
+ 'apollo-people-match': 'Apollo',
23
+ fullenrich: 'FullEnrich',
24
+ 'fullenrich-people-search': 'FullEnrich',
25
+ pdl: 'People Data Labs',
26
+ 'pdl-person-enrich': 'People Data Labs',
27
+ 'pdl-person-identify': 'People Data Labs',
28
+ signalbase: 'SignalBase',
29
+ 'signalbase-funding': 'SignalBase',
30
+ 'signalbase-acquisition': 'SignalBase',
31
+ 'signalbase-hiring': 'SignalBase',
32
+ 'signalbase-job-change': 'SignalBase',
33
+ blitzapi: 'BlitzAPI',
34
+ 'blitzapi-reverse-email': 'BlitzAPI',
35
+ limadata: 'LimaData',
36
+ 'limadata-prospect-filter': 'LimaData',
37
+ 'limadata-prospect-url': 'LimaData',
38
+ 'limadata-work-email': 'LimaData',
39
+ 'limadata-work-email-linkedin': 'LimaData',
40
+ predictleads: 'PredictLeads',
41
+ 'predictleads-financing': 'PredictLeads',
42
+ 'predictleads-news': 'PredictLeads',
43
+ 'predictleads-startup-posts': 'PredictLeads',
44
+ theirstack: 'TheirStack',
45
+ 'theirstack-jobs': 'TheirStack',
46
+ 'theirstack-hiring': 'TheirStack',
47
+ 'theirstack-intent-discovery': 'TheirStack',
48
+ 'theirstack-buying-intents': 'TheirStack',
49
+ sumble: 'Sumble',
50
+ 'sumble-people-find': 'Sumble',
51
+ prospeo: 'Prospeo',
52
+ 'prospeo-search-company': 'Prospeo',
53
+ 'prospeo-search-person': 'Prospeo',
54
+ 'ai-ark': 'AI-ARK',
55
+ 'ai-ark-companies': 'AI-ARK',
56
+ 'ai-ark-people': 'AI-ARK',
57
+ 'ai-ark-reverse-lookup': 'AI-ARK',
58
+ leadsfactory: 'LeadsFactory',
59
+ findymail: 'Findymail',
60
+ 'findymail-search-employees': 'Findymail',
61
+ 'findymail-business-profile': 'Findymail',
62
+ 'findymail-reverse-email': 'Findymail',
63
+ icypeas: 'Icypeas',
64
+ 'icypeas-scrape-profile': 'Icypeas',
65
+ 'icypeas-url-search-profile': 'Icypeas',
66
+ 'icypeas-reverse-email-lookup': 'Icypeas',
67
+ wiza: 'Wiza',
68
+ builtwith: 'BuiltWith',
69
+ openmart: 'Openmart',
70
+ instantly: 'Instantly',
71
+
72
+ // --- LinkupAPI (LinkedIn data) -------------------------------------------
73
+ linkupapi: 'LinkupAPI',
74
+ 'linkupapi-search': 'LinkupAPI',
75
+ 'linkupapi-fundraising': 'LinkupAPI',
76
+ 'linkupapi-hiring': 'LinkupAPI',
77
+ 'linkupapi-search-profiles': 'LinkupAPI',
78
+ 'linkupapi-by-domain': 'LinkupAPI',
79
+ 'linkupapi-by-url': 'LinkupAPI',
80
+ 'linkupapi-profile-enrich': 'LinkupAPI',
81
+ 'linkupapi-email-reverse': 'LinkupAPI',
82
+ 'linkupapi-validate': 'LinkupAPI',
83
+
84
+ // --- search_web ----------------------------------------------------------
85
+ serper: 'Serper',
86
+ exa: 'Exa',
87
+ 'exa-contents': 'Exa',
88
+ jina: 'Jina',
89
+
90
+ // --- search_jobs ---------------------------------------------------------
91
+ career_site_jobs: 'Career Site Jobs',
92
+ linkedin_jobs_api: 'LinkedIn Jobs',
93
+
94
+ // --- search_ads ----------------------------------------------------------
95
+ google_ads: 'Google Ads',
96
+ linkedin_ad_library: 'LinkedIn Ad Library',
97
+ meta_ads: 'Meta Ads',
98
+ twitter_ads: 'X Ads',
99
+ reddit_ads: 'Reddit Ads',
100
+
101
+ // --- search_places / reviews ---------------------------------------------
102
+ google_maps: 'Google Maps',
103
+ google_maps_reviews: 'Google Maps',
104
+
105
+ // --- find_influencers ----------------------------------------------------
106
+ influencers_similar: 'Influencer Discovery',
107
+ influencers_discovery: 'Influencer Discovery',
108
+
109
+ // --- search_reddit -------------------------------------------------------
110
+ reddit: 'Reddit',
111
+
112
+ // --- search_seo (functional sub-actions) ---------------------------------
113
+ kw_search_volume: 'Keyword Search Volume',
114
+ kw_trends: 'Keyword Trends',
115
+ serp_google: 'Google SERP',
116
+ serp_bing: 'Bing SERP',
117
+ serp_youtube: 'YouTube SERP',
118
+ bl_summary: 'Backlink Summary',
119
+ bl_backlinks: 'Backlinks',
120
+ bl_referring: 'Referring Domains',
121
+ domain_tech: 'Domain Technologies',
122
+ domain_whois: 'Domain WHOIS',
123
+ labs_rank_overview: 'Rank Overview',
124
+ labs_ranked_kw: 'Ranked Keywords',
125
+ labs_competitors: 'Competitor Domains',
126
+ labs_kw_ideas: 'Keyword Ideas',
127
+ page_lighthouse: 'Lighthouse Audit',
128
+ page_content: 'Page Content',
129
+ }
130
+
131
+ /**
132
+ * Title-case an unmapped provider ID as a defensive fallback so a newly added
133
+ * provider still renders acceptably (and never leaks a raw slug). E.g.
134
+ * "some-new-vendor" → "Some New Vendor".
135
+ */
136
+ function titleCaseId(id: string): string {
137
+ return id
138
+ .split(/[-_]/)
139
+ .filter(Boolean)
140
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
141
+ .join(' ')
142
+ }
143
+
144
+ /**
145
+ * Map an internal provider ID to a clean, branded display name. Falls back to a
146
+ * title-cased form of the ID when unmapped. Never returns "Apify".
147
+ */
148
+ export function providerDisplayName(id: string): string {
149
+ return DISPLAY_NAMES[id] ?? titleCaseId(id)
150
+ }