@coldiq/mcp 0.1.0 → 0.1.1

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/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # ColdIQ MCP Server
2
+
3
+ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes ColdIQ's B2B data marketplace to AI assistants. Connect it to Claude or any MCP-compatible client to search companies, find and enrich contacts, verify emails, track sales signals, and more — all powered by ColdIQ's unified API.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - A [ColdIQ API key](https://marketplace.coldiq.com)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ cd mcp
14
+ npm install
15
+ npm run build
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ | Variable | Required | Default | Description |
21
+ |---|---|---|---|
22
+ | `COLDIQ_API_KEY` | Yes | — | Your ColdIQ API key |
23
+ | `COLDIQ_API_URL` | No | `https://api.coldiq.com` | Override the API base URL |
24
+ | `COLDIQ_DEBUG` | No | — | Set to `1` to enable debug logging |
25
+
26
+ ## Connecting to Claude
27
+
28
+ ### Claude Desktop
29
+
30
+ Add the following to your `claude_desktop_config.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "coldiq": {
36
+ "command": "node",
37
+ "args": ["/absolute/path/to/mcp/dist/index.js"],
38
+ "env": {
39
+ "COLDIQ_API_KEY": "your_api_key_here"
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Claude Code
47
+
48
+ ```bash
49
+ claude mcp add coldiq -- node /absolute/path/to/mcp/dist/index.js
50
+ ```
51
+
52
+ Then set the env var in your shell or pass it via the config above.
53
+
54
+ ### Running directly
55
+
56
+ ```bash
57
+ COLDIQ_API_KEY=your_key node dist/index.js
58
+ # or during development:
59
+ COLDIQ_API_KEY=your_key npm run dev
60
+ ```
61
+
62
+ ## Available Tools
63
+
64
+ ### Prospecting
65
+
66
+ | Tool | Description |
67
+ |---|---|
68
+ | `search_companies` | Search B2B companies by firmographics, tech stack, funding, headcount, or hiring signals |
69
+ | `find_people` | Find people at companies filtered by job title or department |
70
+ | `find_influencers` | Discover LinkedIn or Twitter influencers in a niche |
71
+
72
+ ### Contact Data
73
+
74
+ | Tool | Description |
75
+ |---|---|
76
+ | `find_email` | Find a professional email from a name and domain (waterfall across providers) |
77
+ | `verify_email` | Check if an email address is deliverable |
78
+ | `find_phone` | Find a phone number for a contact |
79
+
80
+ ### Enrichment
81
+
82
+ | Tool | Description |
83
+ |---|---|
84
+ | `enrich_person` | Enrich a person's profile from a LinkedIn URL, name, or domain |
85
+ | `enrich_company` | Enrich a company's profile from its domain, LinkedIn URL, or name |
86
+
87
+ ### Intelligence & Research
88
+
89
+ | Tool | Description |
90
+ |---|---|
91
+ | `find_signals` | Surface sales signals: funding rounds, acquisitions, job changes, hiring intent, buying intent, news |
92
+ | `search_jobs` | Search live job postings; routes to ATS career sites or LinkedIn based on filters |
93
+ | `search_ads` | Discover active LinkedIn ads from a company |
94
+ | `search_seo` | SERP rankings, traffic data, and technology stack analysis |
95
+ | `search_reddit` | Search Reddit posts and discussions |
96
+
97
+ ### Web & Local
98
+
99
+ | Tool | Description |
100
+ |---|---|
101
+ | `search_web` | General web search (Google or neural/Exa) |
102
+ | `search_places` | Find local businesses and places |
103
+ | `fetch_page_content` | Extract text content or a summary from any URL |
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ # Run unit tests
109
+ npm test
110
+
111
+ # Watch mode
112
+ npm run test:watch
113
+
114
+ # Live integration tests (consumes credits)
115
+ LIVE_TESTS=1 npm run test:gtm
116
+ ```
117
+
118
+ Tests live in `tests/`, mirroring the `src/` structure. See `tests/gtm-scenarios.md` for realistic end-to-end usage scenarios.
119
+
120
+ ## Architecture
121
+
122
+ The server is built on three layers:
123
+
124
+ - **Tools** (`src/tools/`) — MCP tool definitions with Zod input schemas and descriptions
125
+ - **Executor** (`src/executor.ts`) — Waterfall/fallback engine that tries providers in priority order
126
+ - **Registry** (`src/registry.ts`) — Maps each capability to an ordered list of providers with parameter translation, applicability gates, and response normalization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/client.ts CHANGED
@@ -51,10 +51,13 @@ export async function callApi(
51
51
  }
52
52
  return { ok: res.ok, status: res.status, data }
53
53
  } catch (err) {
54
+ if (process.env.COLDIQ_DEBUG) {
55
+ console.error(`[coldiq:debug] network error: ${err instanceof Error ? err.message : String(err)}`)
56
+ }
54
57
  return {
55
58
  ok: false,
56
59
  status: 0,
57
- data: { error: err instanceof Error ? err.message : String(err) },
60
+ data: { error: 'Network error' },
58
61
  }
59
62
  }
60
63
  }
package/src/executor.ts CHANGED
@@ -12,7 +12,7 @@ export interface ExecutionResult {
12
12
 
13
13
  export interface ExecutionError {
14
14
  error: string
15
- providers_tried: Array<{ id: string; status: number; error: string }>
15
+ providers_tried: Array<{ provider: string; status: number; error: string }>
16
16
  }
17
17
 
18
18
  function isExecutionError(result: ExecutionResult | ExecutionError): result is ExecutionError {
@@ -102,9 +102,17 @@ async function executeSingle(
102
102
  input: Record<string, unknown>,
103
103
  ): Promise<{ ok: boolean; status: number; data: unknown; latencyMs: number }> {
104
104
  const start = Date.now()
105
- const payload = provider.mapParams(input)
106
105
 
107
- debug(`${provider.id}: payload = ${JSON.stringify(payload)}`)
106
+ let payload: ReturnType<ProviderEntry['mapParams']>
107
+ try {
108
+ payload = provider.mapParams(input)
109
+ } catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err)
111
+ debug(`${provider.id}: mapParams threw — ${msg}`)
112
+ return { ok: false, status: 0, data: { error: `mapParams_threw: ${msg}` }, latencyMs: Date.now() - start }
113
+ }
114
+
115
+ debug(`${provider.id}: payload_keys = ${Object.keys(payload?.body ?? {}).join(',')}`)
108
116
 
109
117
  let result: { ok: boolean; status: number; data: unknown }
110
118
 
@@ -131,21 +139,43 @@ export async function executeWithFallback(
131
139
  ? getSearchWebProviders(input.search_type === 'neural')
132
140
  : getProviders(capability)
133
141
 
134
- const errors: ExecutionError['providers_tried'] = []
142
+ const errors: Array<{ id: string; status: number; error: string }> = []
135
143
 
136
144
  for (const provider of providers) {
137
- if (provider.isApplicable && !provider.isApplicable(input)) {
138
- debug(`${capability} skipping ${provider.id} (input not applicable)`)
139
- continue
145
+ if (provider.isApplicable) {
146
+ let applicable: boolean
147
+ try {
148
+ applicable = provider.isApplicable(input)
149
+ } catch (err) {
150
+ debug(`${capability} → ${provider.id}: isApplicable threw — ${err instanceof Error ? err.message : String(err)}, skipping`)
151
+ continue
152
+ }
153
+ if (!applicable) {
154
+ debug(`${capability} → skipping ${provider.id} (input not applicable)`)
155
+ continue
156
+ }
140
157
  }
141
158
  log(`${capability} → trying ${provider.id}`)
142
159
  const result = await executeSingle(provider, input)
143
160
 
144
161
  let payload = result.data
145
162
  if (result.ok && provider.postFilter) {
146
- payload = provider.postFilter(payload, input)
163
+ try {
164
+ payload = provider.postFilter(payload, input)
165
+ } catch (err) {
166
+ debug(`${provider.id}: postFilter threw — ${err instanceof Error ? err.message : String(err)}, using raw payload`)
167
+ }
147
168
  }
148
- if (result.ok && provider.hasResult(payload)) {
169
+
170
+ let hasResult: boolean
171
+ try {
172
+ hasResult = result.ok && provider.hasResult(payload)
173
+ } catch (err) {
174
+ debug(`${provider.id}: hasResult threw — ${err instanceof Error ? err.message : String(err)}, treating as no result`)
175
+ hasResult = false
176
+ }
177
+
178
+ if (hasResult) {
149
179
  log(`${capability} → ${provider.id} ✓ (${result.latencyMs}ms)`)
150
180
  return { data: payload, _meta: { provider: provider.id, latencyMs: result.latencyMs } }
151
181
  }
@@ -161,22 +191,14 @@ export async function executeWithFallback(
161
191
  errors.push({ id: provider.id, status: result.status, error: errMsg })
162
192
  }
163
193
 
194
+ debug(`${capability}: all ${errors.length} providers failed — ${JSON.stringify(errors)}`)
195
+ const sanitized = errors.map((e, i) => ({
196
+ provider: `provider_${i + 1}`,
197
+ status: e.status,
198
+ error: e.error.slice(0, 120),
199
+ }))
164
200
  return {
165
201
  error: `All ${errors.length} providers failed for ${capability}`,
166
- providers_tried: errors,
202
+ providers_tried: sanitized,
167
203
  }
168
204
  }
169
-
170
- // ---------------------------------------------------------------------------
171
- // Waterfall execution — try providers until one returns a result
172
- // Used for enrich_email: stop on first provider that finds an email
173
- // ---------------------------------------------------------------------------
174
-
175
- export async function executeWaterfall(
176
- capability: Capability,
177
- input: Record<string, unknown>,
178
- ): Promise<ExecutionResult | ExecutionError> {
179
- // Waterfall is functionally the same as fallback — try each until one succeeds
180
- // The distinction is semantic: waterfall means "I expect most to fail, stop on first hit"
181
- return executeWithFallback(capability, input)
182
- }
package/src/registry.ts CHANGED
@@ -648,79 +648,6 @@ const searchCompaniesProviders: ProviderEntry[] = [
648
648
  isNonEmptyArray(input.industries) ||
649
649
  isNonEmptyArray(input.technologies),
650
650
  },
651
- {
652
- id: 'wiza',
653
- endpoint: '/wiza/prospects/create-prospect-list',
654
- method: 'POST',
655
- priority: 5,
656
- isApplicable: (input) =>
657
- typeof input.min_founded_year === 'number' ||
658
- typeof input.max_founded_year === 'number' ||
659
- isNonEmptyArray(input.funding_stages) ||
660
- typeof input.min_funding_amount === 'number' ||
661
- typeof input.max_funding_amount === 'number',
662
- mapParams: (input) => {
663
- // Wiza filters use [{v: 'value'}] shape. Wiza's prospect endpoints return *people*,
664
- // but create-prospect-list also returns the company set indirectly via its filters.
665
- // For a search_companies waterfall, we treat a non-empty `stats.people` as proof
666
- // that the requested filter set matched at least one company.
667
- const wrap = (vals?: string[]) => (Array.isArray(vals) && vals.length > 0 ? vals.map((v) => ({ v })) : undefined)
668
- const filters: Record<string, unknown> = {}
669
- const industries = [
670
- ...((input.industries as string[] | undefined) ?? []),
671
- ...((input.keywords as string[] | undefined) ?? []),
672
- ]
673
- // Wiza company_location accepts free-text English names — translate ISO codes.
674
- const locations = [
675
- ...((input.locations as string[] | undefined) ?? []),
676
- ...((input.countries as string[] | undefined) ?? []).map(isoCountryToName),
677
- ]
678
- if (industries.length > 0) filters.company_industry = wrap(industries)
679
- if (locations.length > 0) filters.company_location = wrap(locations)
680
- if (typeof input.min_founded_year === 'number') filters.year_founded_start = wrap([String(input.min_founded_year)])
681
- if (typeof input.max_founded_year === 'number') filters.year_founded_end = wrap([String(input.max_founded_year)])
682
- if (isNonEmptyArray(input.funding_stages)) filters.funding_stage = wrap(input.funding_stages as string[])
683
- if (typeof input.min_funding_amount === 'number') filters.funding_min = wrap([String(input.min_funding_amount)])
684
- if (typeof input.max_funding_amount === 'number') filters.funding_max = wrap([String(input.max_funding_amount)])
685
- const limit = Math.min((input.limit as number) ?? 25, 500)
686
- return {
687
- body: {
688
- list: { name: 'mcp-search-companies', max_profiles: limit },
689
- filters,
690
- enrichment_level: 'none',
691
- },
692
- }
693
- },
694
- hasResult: (data) => {
695
- // Final poll response: { status, type, data: { id, status: 'finished', stats: { people } } }
696
- const d = data as Record<string, unknown>
697
- const inner = d.data as Record<string, unknown> | undefined
698
- const stats = inner?.stats as Record<string, unknown> | undefined
699
- const people = stats?.people as number | undefined
700
- return typeof people === 'number' && people > 0
701
- },
702
- async: {
703
- extractId: (response) => {
704
- const r = response as Record<string, unknown>
705
- const inner = r.data as Record<string, unknown> | undefined
706
- const id = inner?.id
707
- if (typeof id === 'number') return String(id)
708
- if (typeof id === 'string' && id.length > 0) return id
709
- throw new Error('Wiza response has no list id')
710
- },
711
- pollEndpoint: (id) => `/wiza/lists/${id}`,
712
- pollIntervalMs: 10_000,
713
- timeoutMs: 300_000, // 5 min
714
- isComplete: (data) => {
715
- // Wiza uses different status strings across endpoints — accept all known terminal
716
- // states (`finished` for individual reveals, `completed` for lists, plus failures).
717
- const d = data as Record<string, unknown>
718
- const inner = d.data as Record<string, unknown> | undefined
719
- const status = inner?.status as string | undefined
720
- return status === 'finished' || status === 'completed' || status === 'failed'
721
- },
722
- },
723
- },
724
651
  {
725
652
  id: 'limadata-prospect-filter',
726
653
  endpoint: '/coldiq/prospect/companies/filter',
@@ -1297,9 +1224,15 @@ const findEmailProviders: ProviderEntry[] = [
1297
1224
  endpoint: '/findymail/search/name',
1298
1225
  method: 'POST',
1299
1226
  priority: 1,
1227
+ isApplicable: (input) =>
1228
+ typeof input.first_name === 'string' && (input.first_name as string).length > 0 &&
1229
+ (
1230
+ (typeof input.domain === 'string' && (input.domain as string).length > 0) ||
1231
+ (typeof input.company_name === 'string' && (input.company_name as string).length > 0)
1232
+ ),
1300
1233
  mapParams: (input) => ({
1301
1234
  body: {
1302
- name: `${input.first_name} ${input.last_name}`,
1235
+ name: [input.first_name, input.last_name].filter(Boolean).join(' '),
1303
1236
  domain: input.domain,
1304
1237
  },
1305
1238
  }),
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { executeWaterfall, isExecutionError } from '../executor.js'
2
+ import { executeWithFallback, isExecutionError } from '../executor.js'
3
3
 
4
4
  export const findEmailName = 'find_email'
5
5
 
@@ -14,8 +14,38 @@ export const findEmailSchema = {
14
14
  linkedin_url: z.string().optional().describe('LinkedIn profile URL — alternative to name+domain'),
15
15
  }
16
16
 
17
+ const inputSchema = z
18
+ .object({
19
+ first_name: z.string().optional(),
20
+ last_name: z.string().optional(),
21
+ domain: z.string().optional(),
22
+ company_name: z.string().optional(),
23
+ linkedin_url: z.string().optional(),
24
+ })
25
+ .superRefine((val, ctx) => {
26
+ const hasLinkedIn = typeof val.linkedin_url === 'string' && val.linkedin_url.length > 0
27
+ const hasName = typeof val.first_name === 'string' && val.first_name.length > 0
28
+ const hasDomain =
29
+ (typeof val.domain === 'string' && val.domain.length > 0) ||
30
+ (typeof val.company_name === 'string' && val.company_name.length > 0)
31
+ if (!hasLinkedIn && !(hasName && hasDomain)) {
32
+ ctx.addIssue({
33
+ code: 'custom',
34
+ message: 'Provide either linkedin_url or both first_name and domain/company_name',
35
+ })
36
+ }
37
+ })
38
+
17
39
  export async function findEmailHandler(input: Record<string, unknown>) {
18
- const result = await executeWaterfall('find_email', input)
40
+ const validation = inputSchema.safeParse(input)
41
+ if (!validation.success) {
42
+ const msg = validation.error.issues.map((i) => i.message).join('; ')
43
+ return {
44
+ content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }, null, 2) }],
45
+ isError: true,
46
+ }
47
+ }
48
+ const result = await executeWithFallback('find_email', input)
19
49
  if (isExecutionError(result)) {
20
50
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
21
51
  }
@@ -91,6 +91,7 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
91
91
  const feBody = {
92
92
  name: 'mcp-enrich-batch',
93
93
  data: afterProspeo.map((p) => ({
94
+ custom_id: p.id,
94
95
  first_name: p.first_name,
95
96
  last_name: p.last_name,
96
97
  domain: p.domain,
@@ -117,9 +118,9 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
117
118
  if (status === 'DONE') {
118
119
  const feItems = pd.data as Array<Record<string, unknown>> | undefined
119
120
  if (Array.isArray(feItems)) {
120
- feItems.forEach((item, idx) => {
121
- const person = afterProspeo[idx]
122
- const hit = results.find((r) => r.id === person?.id)
121
+ feItems.forEach((item) => {
122
+ const personId = item.custom_id as string | undefined
123
+ const hit = personId ? results.find((r) => r.id === personId) : undefined
123
124
  if (!hit || hit.email) return
124
125
  const emails = item.emails as string[] | undefined
125
126
  if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
@@ -23,7 +23,7 @@ export const findSignalsSchema = {
23
23
  companies: z
24
24
  .array(z.string())
25
25
  .optional()
26
- .describe('Company names to filter signals for (e.g. ["ColdIQ", "HubSpot"]). Only used by company-targeted signal types: funding, acquisition, hiring, job_change, intent. Rejected for news and startup_post.'),
26
+ .describe('Company names to filter signals for (e.g. ["ColdIQ", "HubSpot"]). Only used by company-targeted signal types: funding, acquisition, hiring, job_change, intent. Rejected for news and startup_post. Currently only the first entry is forwarded to the upstream provider; pass the most important company first or call the tool once per company.'),
27
27
  domains: z
28
28
  .array(z.string())
29
29
  .optional()
package/test-ads-live.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchAdsHandler } from './src/tools/search-ads.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { enrichCompanyHandler } from './src/tools/enrich-company.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -1,10 +1,11 @@
1
1
  import { initClient } from './src/client.js'
2
- import { enrichEmailHandler } from './src/tools/enrich-email.js'
3
- import { enrichEmailsHandler } from './src/tools/enrich-emails.js'
2
+ import { findEmailHandler } from './src/tools/find-email.js'
3
+ import { findEmailsHandler } from './src/tools/find-emails.js'
4
4
  import { verifyEmailHandler } from './src/tools/verify-email.js'
5
5
 
6
- const API_URL = 'https://api.coldiq.com'
7
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
6
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
7
+ const API_KEY = process.env.COLDIQ_API_KEY
8
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
8
9
 
9
10
  initClient(API_URL, API_KEY)
10
11
 
@@ -41,7 +42,7 @@ async function run(label: string, handler: Handler, input: Record<string, unknow
41
42
  if (Array.isArray(parsed.providers_tried)) {
42
43
  console.log(`PROVIDERS TRIED (${parsed.providers_tried.length}):`)
43
44
  for (const p of parsed.providers_tried) {
44
- console.log(` • ${p.id}: status=${p.status ?? '?'} error=${p.error ?? '-'}`)
45
+ console.log(` • ${p.provider}: status=${p.status ?? '?'} error=${p.error ?? '-'}`)
45
46
  }
46
47
  }
47
48
  return
@@ -73,7 +74,7 @@ async function main() {
73
74
 
74
75
  // Test 1: Happy path — name + domain
75
76
  // Expected: findymail (priority 1) wins. blitzapi + limadata-work-email-linkedin skipped (no linkedin_url).
76
- await run('enrich_email — happy path (name + domain)', enrichEmailHandler, {
77
+ await run('enrich_email — happy path (name + domain)', findEmailHandler, {
77
78
  first_name: 'Michel',
78
79
  last_name: 'Lieben',
79
80
  domain: 'coldiq.com',
@@ -81,7 +82,7 @@ async function main() {
81
82
 
82
83
  // Test 2: Name + company_name, no domain
83
84
  // Expected: limadata-work-email skipped (requires domain). icypeas (priority 2) likely wins via company_name.
84
- await run('enrich_email — name + company_name, no domain (Satya Nadella / Microsoft)', enrichEmailHandler, {
85
+ await run('enrich_email — name + company_name, no domain (Satya Nadella / Microsoft)', findEmailHandler, {
85
86
  first_name: 'Satya',
86
87
  last_name: 'Nadella',
87
88
  company_name: 'Microsoft',
@@ -90,7 +91,7 @@ async function main() {
90
91
  // Test 3: LinkedIn URL + name, no domain
91
92
  // Expected: limadata-work-email skipped (no domain). prospeo (priority 4) likely wins via linkedin_url.
92
93
  // blitzapi + limadata-work-email-linkedin are now eligible (have linkedin_url).
93
- await run('enrich_email — linkedin_url + name, no domain', enrichEmailHandler, {
94
+ await run('enrich_email — linkedin_url + name, no domain', findEmailHandler, {
94
95
  first_name: 'Michel',
95
96
  last_name: 'Lieben',
96
97
  linkedin_url: 'https://www.linkedin.com/in/michel-lieben',
@@ -98,7 +99,7 @@ async function main() {
98
99
 
99
100
  // Test 4: Full input — all 8 providers eligible
100
101
  // Expected: findymail wins. Debug trace shows 0 skips.
101
- await run('enrich_email — full input (all 8 providers eligible)', enrichEmailHandler, {
102
+ await run('enrich_email — full input (all 8 providers eligible)', findEmailHandler, {
102
103
  first_name: 'Michel',
103
104
  last_name: 'Lieben',
104
105
  domain: 'coldiq.com',
@@ -109,7 +110,7 @@ async function main() {
109
110
  // Test 5: Forced fallthrough — fake name at real domain
110
111
  // Expected: providers 1-3 miss (no such person), waterfall marches to prospeo/fullenrich/linkupapi.
111
112
  // Most important scenario for confirming priority order. fullenrich may add 5-10s (async poll).
112
- await run('enrich_email — forced fallthrough (fake name at coldiq.com)', enrichEmailHandler, {
113
+ await run('enrich_email — forced fallthrough (fake name at coldiq.com)', findEmailHandler, {
113
114
  first_name: 'Zerphus',
114
115
  last_name: 'Mclaren',
115
116
  domain: 'coldiq.com',
@@ -117,7 +118,7 @@ async function main() {
117
118
 
118
119
  // Test 6: Total failure — fabricated person + nonexistent domain
119
120
  // Expected: isError=true, providers_tried.length === 6 (skips blitzapi + limadata-work-email-linkedin).
120
- await run('enrich_email — total failure (fake person + nonexistent domain)', enrichEmailHandler, {
121
+ await run('enrich_email — total failure (fake person + nonexistent domain)', findEmailHandler, {
121
122
  first_name: 'Zerphus',
122
123
  last_name: 'Mclaren',
123
124
  domain: 'this-domain-does-not-exist-zxcv.io',
@@ -129,7 +130,7 @@ async function main() {
129
130
  // michel: Prospeo stage 1 via linkedin_url (most likely)
130
131
  // satya: FullEnrich stage 2 or FindyMail stage 3 (no linkedin_url)
131
132
  // fake-person: provider=null expected
132
- await run('enrich_emails — mixed batch (Prospeo → FullEnrich → FindyMail/IcyPeas)', enrichEmailsHandler, {
133
+ await run('enrich_emails — mixed batch (Prospeo → FullEnrich → FindyMail/IcyPeas)', findEmailsHandler, {
133
134
  people: [
134
135
  {
135
136
  id: 'michel',
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { findInfluencersHandler } from './src/tools/find-influencers.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
package/test-jobs-live.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchJobsHandler } from './src/tools/search-jobs.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -17,8 +17,9 @@ import { enrichCompanyHandler } from './src/tools/enrich-company.js'
17
17
  import { findPeopleHandler } from './src/tools/find-people.js'
18
18
  import { searchCompaniesHandler } from './src/tools/search-companies.js'
19
19
 
20
- const API_URL = 'https://api.coldiq.com'
21
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
20
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
21
+ const API_KEY = process.env.COLDIQ_API_KEY
22
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
22
23
 
23
24
  initClient(API_URL, API_KEY)
24
25
 
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { findPhoneHandler } from './src/tools/find-phone.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchPlacesHandler } from './src/tools/search-places.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchRedditHandler } from './src/tools/search-reddit.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchCompaniesHandler } from './src/tools/search-companies.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9
 
package/test-seo-live.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { initClient } from './src/client.js'
2
2
  import { searchSeoHandler } from './src/tools/search-seo.js'
3
3
 
4
- const API_URL = 'https://api.coldiq.com'
5
- const API_KEY = 'ciq_test_3797d4ddc5542ceb'
4
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
5
+ const API_KEY = process.env.COLDIQ_API_KEY
6
+ if (!API_KEY) { console.error('COLDIQ_API_KEY env var is required'); process.exit(1) }
6
7
 
7
8
  initClient(API_URL, API_KEY)
8
9