@coldiq/mcp 0.1.9 → 0.1.11

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.
Files changed (113) hide show
  1. package/dist/executor.d.ts +8 -1
  2. package/dist/executor.d.ts.map +1 -1
  3. package/dist/executor.js +26 -12
  4. package/dist/executor.js.map +1 -1
  5. package/dist/registry.d.ts +9 -0
  6. package/dist/registry.d.ts.map +1 -1
  7. package/dist/registry.js +16 -0
  8. package/dist/registry.js.map +1 -1
  9. package/dist/tools/enrich-company.d.ts +2 -1
  10. package/dist/tools/enrich-company.d.ts.map +1 -1
  11. package/dist/tools/enrich-company.js +10 -3
  12. package/dist/tools/enrich-company.js.map +1 -1
  13. package/dist/tools/enrich-person.d.ts +1 -0
  14. package/dist/tools/enrich-person.d.ts.map +1 -1
  15. package/dist/tools/enrich-person.js +10 -2
  16. package/dist/tools/enrich-person.js.map +1 -1
  17. package/dist/tools/fetch-page-content.d.ts +1 -0
  18. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  19. package/dist/tools/fetch-page-content.js +8 -1
  20. package/dist/tools/fetch-page-content.js.map +1 -1
  21. package/dist/tools/find-email.d.ts +1 -0
  22. package/dist/tools/find-email.d.ts.map +1 -1
  23. package/dist/tools/find-email.js +9 -2
  24. package/dist/tools/find-email.js.map +1 -1
  25. package/dist/tools/find-emails.d.ts +8 -0
  26. package/dist/tools/find-emails.d.ts.map +1 -1
  27. package/dist/tools/find-emails.js +64 -33
  28. package/dist/tools/find-emails.js.map +1 -1
  29. package/dist/tools/find-influencers.d.ts +1 -0
  30. package/dist/tools/find-influencers.d.ts.map +1 -1
  31. package/dist/tools/find-influencers.js +8 -1
  32. package/dist/tools/find-influencers.js.map +1 -1
  33. package/dist/tools/find-people.d.ts +1 -0
  34. package/dist/tools/find-people.d.ts.map +1 -1
  35. package/dist/tools/find-people.js +12 -5
  36. package/dist/tools/find-people.js.map +1 -1
  37. package/dist/tools/find-phone.d.ts +1 -0
  38. package/dist/tools/find-phone.d.ts.map +1 -1
  39. package/dist/tools/find-phone.js +9 -2
  40. package/dist/tools/find-phone.js.map +1 -1
  41. package/dist/tools/find-signals.d.ts +1 -0
  42. package/dist/tools/find-signals.d.ts.map +1 -1
  43. package/dist/tools/find-signals.js +14 -7
  44. package/dist/tools/find-signals.js.map +1 -1
  45. package/dist/tools/search-ads.d.ts +1 -0
  46. package/dist/tools/search-ads.d.ts.map +1 -1
  47. package/dist/tools/search-ads.js +8 -1
  48. package/dist/tools/search-ads.js.map +1 -1
  49. package/dist/tools/search-companies.d.ts +2 -1
  50. package/dist/tools/search-companies.d.ts.map +1 -1
  51. package/dist/tools/search-companies.js +9 -2
  52. package/dist/tools/search-companies.js.map +1 -1
  53. package/dist/tools/search-jobs.d.ts +1 -0
  54. package/dist/tools/search-jobs.d.ts.map +1 -1
  55. package/dist/tools/search-jobs.js +9 -2
  56. package/dist/tools/search-jobs.js.map +1 -1
  57. package/dist/tools/search-places.d.ts +1 -0
  58. package/dist/tools/search-places.d.ts.map +1 -1
  59. package/dist/tools/search-places.js +8 -1
  60. package/dist/tools/search-places.js.map +1 -1
  61. package/dist/tools/search-reddit.d.ts +1 -0
  62. package/dist/tools/search-reddit.d.ts.map +1 -1
  63. package/dist/tools/search-reddit.js +8 -1
  64. package/dist/tools/search-reddit.js.map +1 -1
  65. package/dist/tools/search-seo.d.ts +1 -0
  66. package/dist/tools/search-seo.d.ts.map +1 -1
  67. package/dist/tools/search-seo.js +8 -1
  68. package/dist/tools/search-seo.js.map +1 -1
  69. package/dist/tools/search-web.d.ts +1 -0
  70. package/dist/tools/search-web.d.ts.map +1 -1
  71. package/dist/tools/search-web.js +10 -2
  72. package/dist/tools/search-web.js.map +1 -1
  73. package/dist/tools/verify-email.d.ts +2 -1
  74. package/dist/tools/verify-email.d.ts.map +1 -1
  75. package/dist/tools/verify-email.js +9 -2
  76. package/dist/tools/verify-email.js.map +1 -1
  77. package/dist/utils/fuzzy.d.ts +13 -0
  78. package/dist/utils/fuzzy.d.ts.map +1 -0
  79. package/dist/utils/fuzzy.js +46 -0
  80. package/dist/utils/fuzzy.js.map +1 -0
  81. package/dist/utils/provider-resolver.d.ts +34 -0
  82. package/dist/utils/provider-resolver.d.ts.map +1 -0
  83. package/dist/utils/provider-resolver.js +235 -0
  84. package/dist/utils/provider-resolver.js.map +1 -0
  85. package/package.json +1 -1
  86. package/src/executor.ts +36 -8
  87. package/src/registry.ts +20 -0
  88. package/src/tools/enrich-company.ts +10 -3
  89. package/src/tools/enrich-person.ts +10 -2
  90. package/src/tools/fetch-page-content.ts +8 -1
  91. package/src/tools/find-email.ts +9 -2
  92. package/src/tools/find-emails.ts +78 -41
  93. package/src/tools/find-influencers.ts +8 -1
  94. package/src/tools/find-people.ts +12 -5
  95. package/src/tools/find-phone.ts +9 -2
  96. package/src/tools/find-signals.ts +15 -7
  97. package/src/tools/search-ads.ts +8 -1
  98. package/src/tools/search-companies.ts +9 -2
  99. package/src/tools/search-jobs.ts +9 -2
  100. package/src/tools/search-places.ts +8 -1
  101. package/src/tools/search-reddit.ts +8 -1
  102. package/src/tools/search-seo.ts +8 -1
  103. package/src/tools/search-web.ts +10 -2
  104. package/src/tools/verify-email.ts +9 -2
  105. package/src/utils/fuzzy.ts +57 -0
  106. package/src/utils/provider-resolver.ts +306 -0
  107. package/tests/executor.test.ts +112 -0
  108. package/tests/registry.test.ts +22 -1
  109. package/tests/tools/find-email.test.ts +43 -0
  110. package/tests/tools/find-emails.test.ts +87 -0
  111. package/tests/tools/search-companies.test.ts +40 -0
  112. package/tests/utils/fuzzy.test.ts +63 -0
  113. package/tests/utils/provider-resolver.test.ts +145 -0
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { callApi } from '../client.js'
3
+ import { resolvePreferredProviders, buildAllFailedError, FIND_EMAILS_PROVIDERS } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const findEmailsName = 'find_emails'
5
6
 
@@ -10,7 +11,8 @@ export const findEmailsDescription =
10
11
  'Much faster than calling find_email one-by-one. Max 50 people per call. ' +
11
12
  'Each person needs a unique id to match results back. ' +
12
13
  'Always pass first_name, last_name, and domain — they are the primary enrichment signal. ' +
13
- 'Also pass linkedin_url when available, but never rely on it alone as some providers cannot resolve vanity URLs.'
14
+ 'Also pass linkedin_url when available, but never rely on it alone as some providers cannot resolve vanity URLs. ' +
15
+ 'ColdIQ automatically picks the best waterfall — pass use_providers only if you need specific tools.'
14
16
 
15
17
  export const findEmailsSchema = {
16
18
  people: z
@@ -26,6 +28,7 @@ export const findEmailsSchema = {
26
28
  .min(1)
27
29
  .max(50)
28
30
  .describe('People to find emails for (max 50)'),
31
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically run the best waterfall — recommended for most use cases. Available providers: ${FIND_EMAILS_PROVIDERS.join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
29
32
  }
30
33
 
31
34
  interface PersonInput {
@@ -100,7 +103,10 @@ async function fullEnrichStep(misses: PersonInput[], results: EmailResult[]): Pr
100
103
  }
101
104
  }
102
105
 
103
- async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[]): Promise<void> {
106
+ async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[], allowedProviders?: string[]): Promise<void> {
107
+ const useFindymail = !allowedProviders || allowedProviders.includes('findymail')
108
+ const useIcypeas = !allowedProviders || allowedProviders.includes('icypeas')
109
+
104
110
  await Promise.all(
105
111
  misses.map(async (person) => {
106
112
  const hit = results.find((r) => r.id === person.id)
@@ -109,22 +115,24 @@ async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[
109
115
  // Skip if the parallel FullEnrich branch already filled this person.
110
116
  if (hit.email) return
111
117
 
112
- const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ')
113
-
114
- const fmRes = await callApi('POST', '/findymail/search/name', {
115
- name: fullName,
116
- domain: person.domain,
117
- })
118
- if (!hit.email && fmRes.ok) {
119
- const d = fmRes.data as Record<string, unknown>
120
- if (typeof d.email === 'string' && d.email.includes('@')) {
121
- hit.email = d.email
122
- hit.provider = 'findymail'
123
- return
118
+ if (useFindymail) {
119
+ const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ')
120
+
121
+ const fmRes = await callApi('POST', '/findymail/search/name', {
122
+ name: fullName,
123
+ domain: person.domain,
124
+ })
125
+ if (!hit.email && fmRes.ok) {
126
+ const d = fmRes.data as Record<string, unknown>
127
+ if (typeof d.email === 'string' && d.email.includes('@')) {
128
+ hit.email = d.email
129
+ hit.provider = 'findymail'
130
+ return
131
+ }
124
132
  }
125
133
  }
126
134
 
127
- if (hit.email) return
135
+ if (hit.email || !useIcypeas) return
128
136
 
129
137
  const icyRes = await callApi('POST', '/icypeas/email-search', {
130
138
  firstname: person.first_name,
@@ -149,40 +157,52 @@ async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[
149
157
  }
150
158
 
151
159
  export async function findEmailsHandler(input: Record<string, unknown>) {
152
- const people = input.people as PersonInput[]
153
- const results: EmailResult[] = people.map((p) => ({ id: p.id, email: null, provider: null }))
160
+ const { use_providers: rawUseProviders, ...restInput } = input
161
+ const people = restInput.people as PersonInput[]
154
162
 
155
- // Step 1: Prospeo bulk — 1 call for all people
156
- const bulkBody = {
157
- data: people.map((p) =>
158
- p.linkedin_url
159
- ? { identifier: p.id, linkedin_url: p.linkedin_url }
160
- : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain },
161
- ),
163
+ const resolved = resolvePreferredProviders('find_emails', restInput, rawUseProviders)
164
+ if (!resolved.ok) {
165
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
162
166
  }
163
167
 
164
- const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody)
168
+ const allowedProviders = resolved.providers
169
+ const isConstrained = allowedProviders.length > 0
170
+
171
+ const results: EmailResult[] = people.map((p) => ({ id: p.id, email: null, provider: null }))
165
172
 
166
- if (bulkRes.ok) {
167
- const data = bulkRes.data as {
168
- results?: Array<{
169
- identifier: string
170
- person?: { email?: { email?: string } }
171
- }>
173
+ // Step 1: Prospeo bulk — 1 call for all people
174
+ if (!isConstrained || allowedProviders.includes('prospeo')) {
175
+ const bulkBody = {
176
+ data: people.map((p) =>
177
+ p.linkedin_url
178
+ ? { identifier: p.id, linkedin_url: p.linkedin_url }
179
+ : { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain },
180
+ ),
172
181
  }
173
- for (const item of data.results ?? []) {
174
- const email = item.person?.email?.email
175
- if (typeof email === 'string' && email.includes('@')) {
176
- const hit = results.find((r) => r.id === item.identifier)
177
- if (hit) {
178
- hit.email = email
179
- hit.provider = 'prospeo'
182
+
183
+ const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody)
184
+
185
+ if (bulkRes.ok) {
186
+ const data = bulkRes.data as {
187
+ results?: Array<{
188
+ identifier: string
189
+ person?: { email?: { email?: string } }
190
+ }>
191
+ }
192
+ for (const item of data.results ?? []) {
193
+ const email = item.person?.email?.email
194
+ if (typeof email === 'string' && email.includes('@')) {
195
+ const hit = results.find((r) => r.id === item.identifier)
196
+ if (hit) {
197
+ hit.email = email
198
+ hit.provider = 'prospeo'
199
+ }
180
200
  }
181
201
  }
182
202
  }
183
203
  }
184
204
 
185
- // Steps 2 & 3 run concurrently for Prospeo misses.
205
+ // Steps 2 & 3 run concurrently for misses.
186
206
  // Step 2: FullEnrich bulk async (slow tail, better European coverage)
187
207
  // Step 3: per-person FindyMail → IcyPeas waterfall
188
208
  // Both branches write to the shared `results` array; every write site checks
@@ -191,15 +211,32 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
191
211
  const misses = missesOf(people, results)
192
212
 
193
213
  if (misses.length > 0) {
194
- await Promise.all([fullEnrichStep(misses, results), findymailIcypeasStep(misses, results)])
214
+ const steps: Promise<void>[] = []
215
+ if (!isConstrained || allowedProviders.includes('fullenrich')) {
216
+ steps.push(fullEnrichStep(misses, results))
217
+ }
218
+ if (!isConstrained || allowedProviders.includes('findymail') || allowedProviders.includes('icypeas')) {
219
+ steps.push(findymailIcypeasStep(misses, results, isConstrained ? allowedProviders : undefined))
220
+ }
221
+ if (steps.length > 0) await Promise.all(steps)
195
222
  }
196
223
 
197
224
  const found = results.filter((r) => r.email !== null).length
225
+
226
+ if (isConstrained && found === 0) {
227
+ const err = buildAllFailedError(allowedProviders, 'find_emails', FIND_EMAILS_PROVIDERS)
228
+ return { content: [{ type: 'text' as const, text: JSON.stringify(err, null, 2) }], isError: true }
229
+ }
230
+
231
+ const meta = resolved.matchedFrom && Object.keys(resolved.matchedFrom).length > 0
232
+ ? { matchedFrom: resolved.matchedFrom }
233
+ : undefined
234
+
198
235
  return {
199
236
  content: [
200
237
  {
201
238
  type: 'text' as const,
202
- text: JSON.stringify({ data: { results, found, total: people.length } }, null, 2),
239
+ text: JSON.stringify({ data: { results, found, total: people.length }, ...(meta ? { _meta: meta } : {}) }, null, 2),
203
240
  },
204
241
  ],
205
242
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const findInfluencersName = 'find_influencers'
5
6
 
@@ -23,10 +24,16 @@ export const findInfluencersSchema = {
23
24
  type: z.enum(['creator', 'business']).optional(),
24
25
  handle: z.string().optional()
25
26
  .describe('Find creators similar to this handle. Routes to lookalike search. No @ prefix.'),
27
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_influencers').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
26
28
  }
27
29
 
28
30
  export async function findInfluencersHandler(input: Record<string, unknown>) {
29
- const result = await executeWithFallback('find_influencers', input)
31
+ const { use_providers: rawUseProviders, ...restInput } = input
32
+ const resolved = resolvePreferredProviders('find_influencers', restInput, rawUseProviders)
33
+ if (!resolved.ok) {
34
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
35
+ }
36
+ const result = await executeWithFallback('find_influencers', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
30
37
  if (isExecutionError(result)) {
31
38
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
32
39
  }
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
3
  import { callApi } from '../client.js'
4
4
  import { getProviders } from '../registry.js'
5
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
5
6
 
6
7
  export const findPeopleName = 'find_people'
7
8
 
@@ -24,10 +25,16 @@ export const findPeopleSchema = {
24
25
  locations: z.array(z.string()).optional().describe('Person or company locations'),
25
26
  keywords: z.array(z.string()).optional().describe('Free-text keyword search terms (e.g. ["growth", "AI"])'),
26
27
  limit: z.number().min(1).max(500).default(25).describe('Max results across all companies combined (default: 25, max: 500). For multiple companies multiply: e.g. 10 companies × 5 each = 50.'),
28
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_people').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
27
29
  }
28
30
 
29
31
  export async function findPeopleHandler(input: Record<string, unknown>) {
30
- const result = await executeWithFallback('find_people', input)
32
+ const { use_providers: rawUseProviders, ...restInput } = input
33
+ const resolved = resolvePreferredProviders('find_people', restInput, rawUseProviders)
34
+ if (!resolved.ok) {
35
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
36
+ }
37
+ const result = await executeWithFallback('find_people', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
31
38
  if (isExecutionError(result)) {
32
39
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
33
40
  }
@@ -35,16 +42,16 @@ export async function findPeopleHandler(input: Record<string, unknown>) {
35
42
  // Gap-fill: if LeadsFactory missed some domains, try Apollo for those.
36
43
  // Only runs when domains were the input — if we sent LinkedIn URLs, no_results_domains
37
44
  // is unreliable (LF may resolve arbitrary domains from URLs, e.g. linkedin.com itself).
38
- const inputHadDomains = Array.isArray(input.company_domains) && (input.company_domains as unknown[]).length > 0
39
- const inputHadLinkedInUrls = Array.isArray(input.company_linkedin_urls) && (input.company_linkedin_urls as unknown[]).length > 0
40
- if (result._meta.provider === 'leadsfactory' && inputHadDomains && !inputHadLinkedInUrls) {
45
+ const inputHadDomains = Array.isArray(restInput.company_domains) && (restInput.company_domains as unknown[]).length > 0
46
+ const inputHadLinkedInUrls = Array.isArray(restInput.company_linkedin_urls) && (restInput.company_linkedin_urls as unknown[]).length > 0
47
+ if (resolved.providers.length === 0 && result._meta.provider === 'leadsfactory' && inputHadDomains && !inputHadLinkedInUrls) {
41
48
  const data = result.data as Record<string, unknown>
42
49
  const missedDomains = (data.no_results_domains as string[] | undefined) ?? []
43
50
 
44
51
  if (missedDomains.length > 0) {
45
52
  const apollo = getProviders('find_people').find((p) => p.id === 'apollo')
46
53
  if (apollo) {
47
- const gapInput = { ...input, company_domains: missedDomains, company_linkedin_urls: undefined }
54
+ const gapInput = { ...restInput, company_domains: missedDomains, company_linkedin_urls: undefined }
48
55
  const payload = apollo.mapParams(gapInput)
49
56
  const apolloRes = await callApi(apollo.method, apollo.endpoint, payload.body, payload.queryParams)
50
57
 
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const findPhoneName = 'find_phone'
5
6
 
@@ -12,6 +13,7 @@ export const findPhoneSchema = {
12
13
  last_name: z.string().optional().describe('Last name (required when no LinkedIn URL)'),
13
14
  company_domain: z.string().optional().describe('Company domain, e.g. coldiq.com'),
14
15
  company_name: z.string().optional().describe('Company name (alternative to company_domain)'),
16
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_phone').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
15
17
  }
16
18
 
17
19
  const inputSchema = z
@@ -37,7 +39,12 @@ const inputSchema = z
37
39
  })
38
40
 
39
41
  export async function findPhoneHandler(input: Record<string, unknown>) {
40
- const validation = inputSchema.safeParse(input)
42
+ const { use_providers: rawUseProviders, ...restInput } = input
43
+ const resolved = resolvePreferredProviders('find_phone', restInput, rawUseProviders)
44
+ if (!resolved.ok) {
45
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
46
+ }
47
+ const validation = inputSchema.safeParse(restInput)
41
48
  if (!validation.success) {
42
49
  const msg = validation.error.issues.map((i) => i.message).join('; ')
43
50
  return {
@@ -45,7 +52,7 @@ export async function findPhoneHandler(input: Record<string, unknown>) {
45
52
  isError: true,
46
53
  }
47
54
  }
48
- const result = await executeWithFallback('find_phone', input)
55
+ const result = await executeWithFallback('find_phone', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
49
56
  if (isExecutionError(result)) {
50
57
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
51
58
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const findSignalsName = 'find_signals'
5
6
 
@@ -47,13 +48,15 @@ export const findSignalsSchema = {
47
48
  .max(100)
48
49
  .default(25)
49
50
  .describe('Maximum number of signals to return (1–100).'),
51
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_signals').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
50
52
  }
51
53
 
52
54
  export async function findSignalsHandler(input: Record<string, unknown>) {
53
- const hasCompanies = Array.isArray(input.companies) && (input.companies as unknown[]).length > 0
54
- const hasDomains = Array.isArray(input.domains) && (input.domains as unknown[]).length > 0
55
+ const { use_providers: rawUseProviders, ...restInput } = input
56
+ const hasCompanies = Array.isArray(restInput.companies) && (restInput.companies as unknown[]).length > 0
57
+ const hasDomains = Array.isArray(restInput.domains) && (restInput.domains as unknown[]).length > 0
55
58
 
56
- if (input.signal_type === 'intent' && !hasCompanies && !hasDomains) {
59
+ if (restInput.signal_type === 'intent' && !hasCompanies && !hasDomains) {
57
60
  return {
58
61
  content: [{
59
62
  type: 'text' as const,
@@ -63,7 +66,7 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
63
66
  }
64
67
  }
65
68
 
66
- if ((input.signal_type === 'news' || input.signal_type === 'startup_post') && (hasCompanies || hasDomains)) {
69
+ if ((restInput.signal_type === 'news' || restInput.signal_type === 'startup_post') && (hasCompanies || hasDomains)) {
67
70
  return {
68
71
  content: [{
69
72
  type: 'text' as const,
@@ -75,14 +78,19 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
75
78
  }
76
79
  }
77
80
 
78
- const result = await executeWithFallback('find_signals', input)
81
+ const resolved = resolvePreferredProviders('find_signals', restInput, rawUseProviders)
82
+ if (!resolved.ok) {
83
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
84
+ }
85
+
86
+ const result = await executeWithFallback('find_signals', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
79
87
  if (isExecutionError(result)) {
80
88
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
81
89
  }
82
90
 
83
91
  // buying_intents upstream has no limit param — truncate to requested limit
84
- if (input.signal_type === 'intent') {
85
- const limit = typeof input.limit === 'number' ? input.limit : 25
92
+ if (restInput.signal_type === 'intent') {
93
+ const limit = typeof restInput.limit === 'number' ? restInput.limit : 25
86
94
  const typed = result as { data?: { data?: unknown[] } }
87
95
  if (Array.isArray(typed.data?.data)) {
88
96
  typed.data!.data = typed.data!.data.slice(0, limit)
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchAdsName = 'search_ads'
5
6
 
@@ -33,10 +34,16 @@ export const searchAdsSchema = {
33
34
  objective_type: z.enum(['AWARENESS', 'CONVERSIONS', 'APP_INSTALLS', 'TRAFFIC', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('Reddit only.'),
34
35
 
35
36
  platform: z.enum(['google', 'linkedin', 'meta', 'twitter', 'reddit']).optional().describe('Pin to one platform; skips all others. Useful for cross-platform comparison via separate calls.'),
37
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_ads').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
36
38
  }
37
39
 
38
40
  export async function searchAdsHandler(input: Record<string, unknown>) {
39
- const result = await executeWithFallback('search_ads', input)
41
+ const { use_providers: rawUseProviders, ...restInput } = input
42
+ const resolved = resolvePreferredProviders('search_ads', restInput, rawUseProviders)
43
+ if (!resolved.ok) {
44
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
45
+ }
46
+ const result = await executeWithFallback('search_ads', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
40
47
  if (isExecutionError(result)) {
41
48
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
42
49
  }
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchCompaniesName = 'search_companies'
5
6
 
6
7
  export const searchCompaniesDescription =
7
- 'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. Default limit: 25.'
8
+ 'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25.'
8
9
 
9
10
  export const searchCompaniesSchema = {
10
11
  keywords: z.array(z.string()).optional().describe('Industry keywords or topics (e.g. ["SaaS", "fintech"])'),
@@ -30,10 +31,16 @@ export const searchCompaniesSchema = {
30
31
  min_workforce_growth_pct: z.number().optional().describe('Minimum workforce growth % over the past 12 months (e.g. 10 for 10%)'),
31
32
  linkedin_search_url: z.string().optional().describe('LinkedIn Sales Navigator company search URL — when provided, enables URL-based prospect discovery as a final fallback. Most users should leave this unset.'),
32
33
  limit: z.number().min(1).max(100).default(25).describe('Max results to return (default: 25, max: 100)'),
34
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_companies').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
33
35
  }
34
36
 
35
37
  export async function searchCompaniesHandler(input: Record<string, unknown>) {
36
- const result = await executeWithFallback('search_companies', input)
38
+ const { use_providers: rawUseProviders, ...restInput } = input
39
+ const resolved = resolvePreferredProviders('search_companies', restInput, rawUseProviders)
40
+ if (!resolved.ok) {
41
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
42
+ }
43
+ const result = await executeWithFallback('search_companies', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
37
44
  if (isExecutionError(result)) {
38
45
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
39
46
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  const _CAREER_SITE_ONLY = ['ats_slugs', 'exclude_ats_slugs', 'company_domains', 'exclude_company_domains']
5
6
  const _LINKEDIN_ONLY = ['seniority_levels', 'industries', 'organization_slugs', 'exclude_organization_slugs', 'min_employees', 'max_employees', 'easy_apply_only', 'exclude_easy_apply']
@@ -54,10 +55,12 @@ export const searchJobsSchema = {
54
55
  max_employees: z.number().int().min(1).optional(),
55
56
  easy_apply_only: z.boolean().optional().describe('Only LinkedIn Easy Apply jobs. Routes to LinkedIn source.'),
56
57
  exclude_easy_apply: z.boolean().optional().describe('Exclude LinkedIn Easy Apply jobs. Routes to LinkedIn source.'),
58
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_jobs').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
57
59
  }
58
60
 
59
61
  export async function searchJobsHandler(input: Record<string, unknown>) {
60
- if (_isFilterSet(input, _CAREER_SITE_ONLY) && _isFilterSet(input, _LINKEDIN_ONLY)) {
62
+ const { use_providers: rawUseProviders, ...restInput } = input
63
+ if (_isFilterSet(restInput, _CAREER_SITE_ONLY) && _isFilterSet(restInput, _LINKEDIN_ONLY)) {
61
64
  const err = {
62
65
  error:
63
66
  'Contradictory filters: ATS/domain filters (ats_slugs, company_domains) are Career Site only, ' +
@@ -65,7 +68,11 @@ export async function searchJobsHandler(input: Record<string, unknown>) {
65
68
  }
66
69
  return { content: [{ type: 'text' as const, text: JSON.stringify(err, null, 2) }], isError: true }
67
70
  }
68
- const result = await executeWithFallback('search_jobs', input)
71
+ const resolved = resolvePreferredProviders('search_jobs', restInput, rawUseProviders)
72
+ if (!resolved.ok) {
73
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
74
+ }
75
+ const result = await executeWithFallback('search_jobs', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
69
76
  if (isExecutionError(result)) {
70
77
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
71
78
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchPlacesName = 'search_places'
5
6
 
@@ -41,10 +42,16 @@ export const searchPlacesSchema = {
41
42
  include_opening_hours: z.boolean().optional().describe('Google Maps only. Routes to Google Maps only when set.'),
42
43
  include_additional_info: z.boolean().optional().describe('Google Maps only. Adds amenities/accessibility fields. Routes to Google Maps only when set.'),
43
44
  language: z.string().optional().describe('Google Maps only. ISO 639-1 (e.g. "en", "fr"). Routes to Google Maps only when set.'),
45
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_places').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
44
46
  }
45
47
 
46
48
  export async function searchPlacesHandler(input: Record<string, unknown>) {
47
- const result = await executeWithFallback('search_places', input)
49
+ const { use_providers: rawUseProviders, ...restInput } = input
50
+ const resolved = resolvePreferredProviders('search_places', restInput, rawUseProviders)
51
+ if (!resolved.ok) {
52
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
53
+ }
54
+ const result = await executeWithFallback('search_places', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
48
55
  if (isExecutionError(result)) {
49
56
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
50
57
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchRedditName = 'search_reddit'
5
6
 
@@ -23,10 +24,16 @@ export const searchRedditSchema = {
23
24
  .describe('Max comments per post when type=comments.'),
24
25
  include_comments: z.boolean().optional()
25
26
  .describe('Include comments alongside posts.'),
27
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_reddit').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
26
28
  }
27
29
 
28
30
  export async function searchRedditHandler(input: Record<string, unknown>) {
29
- const result = await executeWithFallback('search_reddit', input)
31
+ const { use_providers: rawUseProviders, ...restInput } = input
32
+ const resolved = resolvePreferredProviders('search_reddit', restInput, rawUseProviders)
33
+ if (!resolved.ok) {
34
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
35
+ }
36
+ const result = await executeWithFallback('search_reddit', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
30
37
  if (isExecutionError(result)) {
31
38
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
32
39
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchSeoName = 'search_seo'
5
6
 
@@ -48,10 +49,16 @@ export const searchSeoSchema = {
48
49
  .describe('page: "lighthouse" (Core Web Vitals audit) or "content" (parse page text).'),
49
50
  enable_javascript: z.boolean().optional(),
50
51
  full_data: z.boolean().optional().describe('Lighthouse only: include full audit data.'),
52
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_seo').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
51
53
  }
52
54
 
53
55
  export async function searchSeoHandler(input: Record<string, unknown>) {
54
- const result = await executeWithFallback('search_seo', input)
56
+ const { use_providers: rawUseProviders, ...restInput } = input
57
+ const resolved = resolvePreferredProviders('search_seo', restInput, rawUseProviders)
58
+ if (!resolved.ok) {
59
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
60
+ }
61
+ const result = await executeWithFallback('search_seo', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
55
62
  if (isExecutionError(result)) {
56
63
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
57
64
  }
@@ -1,22 +1,30 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const searchWebName = 'search_web'
5
6
 
6
7
  export const searchWebDescription =
7
8
  'Search the web for market research, company news, or general lookups. Supports Google search (default) and neural/semantic search via Exa. ' +
8
9
  'NEVER use this to find, verify, or look up people, contacts, LinkedIn profiles, or executives — even as a fallback when find_people returns uncertain results. ' +
9
- 'find_people is the only tool for people lookups; do not supplement or replace it with web search.'
10
+ 'find_people is the only tool for people lookups; do not supplement or replace it with web search. ' +
11
+ 'ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool.'
10
12
 
11
13
  export const searchWebSchema = {
12
14
  query: z.string().describe('Search query'),
13
15
  num_results: z.number().min(1).max(100).default(10).describe('Number of results (default: 10, max: 100)'),
14
16
  country: z.string().optional().describe('Country code for geo-targeting (e.g. "us", "fr")'),
15
17
  search_type: z.enum(['general', 'neural']).default('general').describe('"general" for Google search (default), "neural" for semantic search via Exa'),
18
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_web').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
16
19
  }
17
20
 
18
21
  export async function searchWebHandler(input: Record<string, unknown>) {
19
- const result = await executeWithFallback('search_web', input)
22
+ const { use_providers: rawUseProviders, ...restInput } = input
23
+ const resolved = resolvePreferredProviders('search_web', restInput, rawUseProviders)
24
+ if (!resolved.ok) {
25
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
26
+ }
27
+ const result = await executeWithFallback('search_web', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
20
28
  if (isExecutionError(result)) {
21
29
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
22
30
  }
@@ -1,17 +1,24 @@
1
1
  import { z } from 'zod'
2
2
  import { executeWithFallback, isExecutionError } from '../executor.js'
3
+ import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
3
4
 
4
5
  export const verifyEmailName = 'verify_email'
5
6
 
6
7
  export const verifyEmailDescription =
7
- 'Check whether an email address is valid and deliverable. Returns verification status (valid, invalid, catch-all, unknown). Use before cold outreach to protect sender reputation.'
8
+ 'Check whether an email address is valid and deliverable. Returns verification status (valid, invalid, catch-all, unknown). Use before cold outreach to protect sender reputation. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool.'
8
9
 
9
10
  export const verifyEmailSchema = {
10
11
  email: z.string().email().describe('Email address to verify'),
12
+ use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('verify_email').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
11
13
  }
12
14
 
13
15
  export async function verifyEmailHandler(input: Record<string, unknown>) {
14
- const result = await executeWithFallback('verify_email', input)
16
+ const { use_providers: rawUseProviders, ...restInput } = input
17
+ const resolved = resolvePreferredProviders('verify_email', restInput, rawUseProviders)
18
+ if (!resolved.ok) {
19
+ return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error, null, 2) }], isError: true }
20
+ }
21
+ const result = await executeWithFallback('verify_email', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
15
22
  if (isExecutionError(result)) {
16
23
  return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], isError: true }
17
24
  }