@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 +126 -0
- package/package.json +1 -1
- package/src/client.ts +4 -1
- package/src/executor.ts +46 -24
- package/src/registry.ts +7 -74
- package/src/tools/find-email.ts +32 -2
- package/src/tools/find-emails.ts +4 -3
- package/src/tools/find-signals.ts +1 -1
- package/test-ads-live.ts +3 -2
- package/test-company-live.ts +3 -2
- package/test-email-live.ts +13 -12
- package/test-influencers-live.ts +3 -2
- package/test-jobs-live.ts +3 -2
- package/test-linkupapi-live.ts +3 -2
- package/test-phone-live.ts +3 -2
- package/test-places-live.ts +3 -2
- package/test-reddit-live.ts +3 -2
- package/test-search-live.ts +3 -2
- package/test-seo-live.ts +3 -2
- package/test-web-live.ts +3 -2
- package/tests/client.test.ts +1 -1
- package/tests/executor.test.ts +227 -0
- package/tests/registry.test.ts +1 -77
- package/tests/tools/{enrich-email.test.ts → find-email.test.ts} +5 -5
- package/tests/tools/{enrich-emails.test.ts → find-emails.test.ts} +16 -15
- package/tests/tools/find-people.test.ts +12 -12
- package/tests/tools/search-places.test.ts +4 -4
- package/tests/tools/search-reddit.test.ts +6 -6
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
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:
|
|
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<{
|
|
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
|
-
|
|
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:
|
|
142
|
+
const errors: Array<{ id: string; status: number; error: string }> = []
|
|
135
143
|
|
|
136
144
|
for (const provider of providers) {
|
|
137
|
-
if (provider.isApplicable
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1235
|
+
name: [input.first_name, input.last_name].filter(Boolean).join(' '),
|
|
1303
1236
|
domain: input.domain,
|
|
1304
1237
|
},
|
|
1305
1238
|
}),
|
package/src/tools/find-email.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
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
|
|
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
|
}
|
package/src/tools/find-emails.ts
CHANGED
|
@@ -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
|
|
121
|
-
const
|
|
122
|
-
const hit = results.find((r) => r.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 =
|
|
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-company-live.ts
CHANGED
|
@@ -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 =
|
|
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-email-live.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { initClient } from './src/client.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 =
|
|
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.
|
|
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)',
|
|
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)',
|
|
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',
|
|
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)',
|
|
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)',
|
|
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)',
|
|
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)',
|
|
133
|
+
await run('enrich_emails — mixed batch (Prospeo → FullEnrich → FindyMail/IcyPeas)', findEmailsHandler, {
|
|
133
134
|
people: [
|
|
134
135
|
{
|
|
135
136
|
id: 'michel',
|
package/test-influencers-live.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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-linkupapi-live.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
package/test-phone-live.ts
CHANGED
|
@@ -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 =
|
|
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-places-live.ts
CHANGED
|
@@ -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 =
|
|
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-reddit-live.ts
CHANGED
|
@@ -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 =
|
|
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-search-live.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|