@coldiq/mcp 0.1.8 → 0.1.10

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/executor.d.ts +8 -1
  3. package/dist/executor.d.ts.map +1 -1
  4. package/dist/executor.js +45 -18
  5. package/dist/executor.js.map +1 -1
  6. package/dist/registry.d.ts +9 -0
  7. package/dist/registry.d.ts.map +1 -1
  8. package/dist/registry.js +16 -0
  9. package/dist/registry.js.map +1 -1
  10. package/dist/tools/enrich-company.d.ts +2 -1
  11. package/dist/tools/enrich-company.d.ts.map +1 -1
  12. package/dist/tools/enrich-company.js +10 -3
  13. package/dist/tools/enrich-company.js.map +1 -1
  14. package/dist/tools/enrich-person.d.ts +1 -0
  15. package/dist/tools/enrich-person.d.ts.map +1 -1
  16. package/dist/tools/enrich-person.js +10 -2
  17. package/dist/tools/enrich-person.js.map +1 -1
  18. package/dist/tools/fetch-page-content.d.ts +1 -0
  19. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  20. package/dist/tools/fetch-page-content.js +8 -1
  21. package/dist/tools/fetch-page-content.js.map +1 -1
  22. package/dist/tools/find-email.d.ts +1 -0
  23. package/dist/tools/find-email.d.ts.map +1 -1
  24. package/dist/tools/find-email.js +9 -2
  25. package/dist/tools/find-email.js.map +1 -1
  26. package/dist/tools/find-emails.d.ts +8 -0
  27. package/dist/tools/find-emails.d.ts.map +1 -1
  28. package/dist/tools/find-emails.js +64 -33
  29. package/dist/tools/find-emails.js.map +1 -1
  30. package/dist/tools/find-influencers.d.ts +1 -0
  31. package/dist/tools/find-influencers.d.ts.map +1 -1
  32. package/dist/tools/find-influencers.js +8 -1
  33. package/dist/tools/find-influencers.js.map +1 -1
  34. package/dist/tools/find-people.d.ts +1 -0
  35. package/dist/tools/find-people.d.ts.map +1 -1
  36. package/dist/tools/find-people.js +12 -5
  37. package/dist/tools/find-people.js.map +1 -1
  38. package/dist/tools/find-phone.d.ts +1 -0
  39. package/dist/tools/find-phone.d.ts.map +1 -1
  40. package/dist/tools/find-phone.js +9 -2
  41. package/dist/tools/find-phone.js.map +1 -1
  42. package/dist/tools/find-signals.d.ts +1 -0
  43. package/dist/tools/find-signals.d.ts.map +1 -1
  44. package/dist/tools/find-signals.js +14 -7
  45. package/dist/tools/find-signals.js.map +1 -1
  46. package/dist/tools/search-ads.d.ts +1 -0
  47. package/dist/tools/search-ads.d.ts.map +1 -1
  48. package/dist/tools/search-ads.js +8 -1
  49. package/dist/tools/search-ads.js.map +1 -1
  50. package/dist/tools/search-companies.d.ts +2 -1
  51. package/dist/tools/search-companies.d.ts.map +1 -1
  52. package/dist/tools/search-companies.js +9 -2
  53. package/dist/tools/search-companies.js.map +1 -1
  54. package/dist/tools/search-jobs.d.ts +1 -0
  55. package/dist/tools/search-jobs.d.ts.map +1 -1
  56. package/dist/tools/search-jobs.js +9 -2
  57. package/dist/tools/search-jobs.js.map +1 -1
  58. package/dist/tools/search-places.d.ts +1 -0
  59. package/dist/tools/search-places.d.ts.map +1 -1
  60. package/dist/tools/search-places.js +8 -1
  61. package/dist/tools/search-places.js.map +1 -1
  62. package/dist/tools/search-reddit.d.ts +1 -0
  63. package/dist/tools/search-reddit.d.ts.map +1 -1
  64. package/dist/tools/search-reddit.js +8 -1
  65. package/dist/tools/search-reddit.js.map +1 -1
  66. package/dist/tools/search-seo.d.ts +1 -0
  67. package/dist/tools/search-seo.d.ts.map +1 -1
  68. package/dist/tools/search-seo.js +8 -1
  69. package/dist/tools/search-seo.js.map +1 -1
  70. package/dist/tools/search-web.d.ts +1 -0
  71. package/dist/tools/search-web.d.ts.map +1 -1
  72. package/dist/tools/search-web.js +10 -2
  73. package/dist/tools/search-web.js.map +1 -1
  74. package/dist/tools/verify-email.d.ts +2 -1
  75. package/dist/tools/verify-email.d.ts.map +1 -1
  76. package/dist/tools/verify-email.js +9 -2
  77. package/dist/tools/verify-email.js.map +1 -1
  78. package/dist/utils/fuzzy.d.ts +13 -0
  79. package/dist/utils/fuzzy.d.ts.map +1 -0
  80. package/dist/utils/fuzzy.js +46 -0
  81. package/dist/utils/fuzzy.js.map +1 -0
  82. package/dist/utils/provider-resolver.d.ts +34 -0
  83. package/dist/utils/provider-resolver.d.ts.map +1 -0
  84. package/dist/utils/provider-resolver.js +235 -0
  85. package/dist/utils/provider-resolver.js.map +1 -0
  86. package/package.json +1 -1
  87. package/src/executor.ts +55 -14
  88. package/src/registry.ts +20 -0
  89. package/src/tools/enrich-company.ts +10 -3
  90. package/src/tools/enrich-person.ts +10 -2
  91. package/src/tools/fetch-page-content.ts +8 -1
  92. package/src/tools/find-email.ts +9 -2
  93. package/src/tools/find-emails.ts +78 -41
  94. package/src/tools/find-influencers.ts +8 -1
  95. package/src/tools/find-people.ts +12 -5
  96. package/src/tools/find-phone.ts +9 -2
  97. package/src/tools/find-signals.ts +15 -7
  98. package/src/tools/search-ads.ts +8 -1
  99. package/src/tools/search-companies.ts +9 -2
  100. package/src/tools/search-jobs.ts +9 -2
  101. package/src/tools/search-places.ts +8 -1
  102. package/src/tools/search-reddit.ts +8 -1
  103. package/src/tools/search-seo.ts +8 -1
  104. package/src/tools/search-web.ts +10 -2
  105. package/src/tools/verify-email.ts +9 -2
  106. package/src/utils/fuzzy.ts +57 -0
  107. package/src/utils/provider-resolver.ts +306 -0
  108. package/tests/executor.test.ts +184 -4
  109. package/tests/registry.test.ts +22 -1
  110. package/tests/tools/find-email.test.ts +43 -0
  111. package/tests/tools/find-emails.test.ts +87 -0
  112. package/tests/tools/search-companies.test.ts +40 -0
  113. package/tests/utils/fuzzy.test.ts +63 -0
  114. package/tests/utils/provider-resolver.test.ts +145 -0
@@ -313,3 +313,90 @@ interface EmailResult {
313
313
  email: string | null
314
314
  provider: string | null
315
315
  }
316
+
317
+ describe('find_emails handler — use_providers', () => {
318
+ const originalFetch = globalThis.fetch
319
+
320
+ beforeEach(() => {
321
+ initClient('http://test-api.local', 'test-key')
322
+ })
323
+
324
+ afterEach(() => {
325
+ globalThis.fetch = originalFetch
326
+ })
327
+
328
+ it('Scenario A — pinned to prospeo only, skips fallback providers', async () => {
329
+ let findymailCalled = false
330
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
331
+ const u = url.toString()
332
+ if (u.includes('/findymail')) findymailCalled = true
333
+ if (u.includes('/prospeo/bulk-enrich-person')) {
334
+ return new Response(JSON.stringify({
335
+ error: false,
336
+ results: [{ identifier: 'p1', person: { email: { email: 'alice@example.com' } } }],
337
+ }), { status: 200 })
338
+ }
339
+ return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
340
+ }) as typeof fetch
341
+
342
+ const result = await findEmailsHandler({
343
+ people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
344
+ use_providers: ['prospeo'],
345
+ })
346
+
347
+ expect(result.isError).toBeUndefined()
348
+ expect(findymailCalled).toBe(false)
349
+ const parsed = JSON.parse(result.content[0].text)
350
+ expect(parsed.data.results[0]).toMatchObject({ provider: 'prospeo' })
351
+ })
352
+
353
+ it('Scenario H — all pinned providers find nothing → returns isError with pushback message', async () => {
354
+ globalThis.fetch = vi.fn(async () =>
355
+ new Response(JSON.stringify({ error: false, results: [] }), { status: 200 })
356
+ ) as typeof fetch
357
+
358
+ const result = await findEmailsHandler({
359
+ people: [{ id: 'p1', first_name: 'Unknown', domain: 'nowhere.xyz' }],
360
+ use_providers: ['findymail'],
361
+ })
362
+
363
+ expect(result.isError).toBe(true)
364
+ const parsed = JSON.parse(result.content[0].text)
365
+ expect(parsed.error).toContain('Couldn\'t fulfill')
366
+ expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
367
+ })
368
+
369
+ it('Scenario E — unrecognized provider name returns isError', async () => {
370
+ const result = await findEmailsHandler({
371
+ people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
372
+ use_providers: ['wiza'],
373
+ })
374
+
375
+ expect(result.isError).toBe(true)
376
+ const parsed = JSON.parse(result.content[0].text)
377
+ expect(parsed.error).toContain("'wiza' is not a recognized provider")
378
+ expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
379
+ })
380
+
381
+ it('Scenario G — ordered providers: first succeeds, second skipped', async () => {
382
+ let icypeasCalled = false
383
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
384
+ const u = url.toString()
385
+ if (u.includes('/icypeas')) icypeasCalled = true
386
+ if (u.includes('/findymail/search/name')) {
387
+ return new Response(JSON.stringify({ email: 'alice@example.com' }), { status: 200 })
388
+ }
389
+ return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
390
+ }) as typeof fetch
391
+
392
+ const result = await findEmailsHandler({
393
+ people: [{ id: 'p1', first_name: 'Alice', domain: 'example.com' }],
394
+ use_providers: ['findymail', 'icypeas'],
395
+ })
396
+
397
+ expect(result.isError).toBeUndefined()
398
+ const parsed = JSON.parse(result.content[0].text)
399
+ expect(parsed.data.results[0]).toMatchObject({ provider: 'findymail', email: 'alice@example.com' })
400
+ expect(icypeasCalled).toBe(false)
401
+ })
402
+ })
@@ -55,4 +55,44 @@ describe('search_companies handler', () => {
55
55
  expect(parsed.error).toContain('All')
56
56
  expect(parsed.providers_tried.length).toBeGreaterThan(0)
57
57
  })
58
+
59
+ describe('use_providers', () => {
60
+ it('Scenario A — pinned provider succeeds, skips others', async () => {
61
+ let callCount = 0
62
+ globalThis.fetch = vi.fn(async () => {
63
+ callCount++
64
+ return new Response(JSON.stringify({ data: [{ name: 'ColdIQ' }] }), { status: 200 })
65
+ }) as typeof fetch
66
+
67
+ const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['companyenrich'] })
68
+
69
+ expect(result.isError).toBeUndefined()
70
+ expect(callCount).toBe(1)
71
+ const parsed = JSON.parse(result.content[0].text)
72
+ expect(parsed._meta.provider).toBe('companyenrich')
73
+ })
74
+
75
+ it('Scenario E — unrecognized provider returns isError with helpful message', async () => {
76
+ const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['salesforce'] })
77
+
78
+ expect(result.isError).toBe(true)
79
+ const parsed = JSON.parse(result.content[0].text)
80
+ expect(parsed.error).toContain("'salesforce' is not a recognized provider")
81
+ expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
82
+ expect(parsed.available_providers).toBeInstanceOf(Array)
83
+ })
84
+
85
+ it('Scenario D — fuzzy match resolves typo and surfaces matchedFrom', async () => {
86
+ globalThis.fetch = vi.fn(async () =>
87
+ new Response(JSON.stringify({ data: [{ name: 'ColdIQ' }] }), { status: 200 })
88
+ ) as typeof fetch
89
+
90
+ const result = await searchCompaniesHandler({ countries: ['FR'], limit: 5, use_providers: ['fullenrch'] })
91
+
92
+ expect(result.isError).toBeUndefined()
93
+ const parsed = JSON.parse(result.content[0].text)
94
+ expect(parsed._meta.provider).toBe('fullenrich')
95
+ expect(parsed._meta.matchedFrom).toEqual({ fullenrch: 'fullenrich' })
96
+ })
97
+ })
58
98
  })
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { normalize, fuzzyMatch } from '../../src/utils/fuzzy.js'
3
+
4
+ describe('normalize', () => {
5
+ it('lowercases', () => {
6
+ expect(normalize('FullEnrich')).toBe('fullenrich')
7
+ })
8
+
9
+ it('strips hyphens', () => {
10
+ expect(normalize('ai-ark')).toBe('aiark')
11
+ expect(normalize('find-email')).toBe('findemail')
12
+ })
13
+
14
+ it('strips spaces', () => {
15
+ expect(normalize('blitz api')).toBe('blitzapi')
16
+ })
17
+
18
+ it('strips underscores', () => {
19
+ expect(normalize('ai_ark')).toBe('aiark')
20
+ })
21
+ })
22
+
23
+ describe('fuzzyMatch', () => {
24
+ const candidates = ['prospeo', 'fullenrich', 'findymail', 'icypeas', 'wiza', 'ai-ark', 'limadata']
25
+
26
+ it('exact match', () => {
27
+ const r = fuzzyMatch('prospeo', candidates)
28
+ expect(r).toEqual({ matched: 'prospeo', via: 'exact' })
29
+ })
30
+
31
+ it('normalized match (case)', () => {
32
+ const r = fuzzyMatch('Prospeo', candidates)
33
+ expect(r).toEqual({ matched: 'prospeo', via: 'normalized' })
34
+ })
35
+
36
+ it('normalized match (punctuation stripped)', () => {
37
+ const r = fuzzyMatch('ai_ark', candidates)
38
+ expect(r).toEqual({ matched: 'ai-ark', via: 'normalized' })
39
+ })
40
+
41
+ it('fuzzy match (1 typo)', () => {
42
+ const r = fuzzyMatch('prospec', candidates)
43
+ expect(r).toEqual({ matched: 'prospeo', via: 'fuzzy' })
44
+ })
45
+
46
+ it('fuzzy match (1 char addition in longer name)', () => {
47
+ const r = fuzzyMatch('fullenrichx', candidates)
48
+ expect(r?.matched).toBe('fullenrich')
49
+ expect(r?.via).toBe('fuzzy')
50
+ })
51
+
52
+ it('no match for clearly wrong name', () => {
53
+ expect(fuzzyMatch('salesforce', candidates)).toBeNull()
54
+ })
55
+
56
+ it('no match for very short string with no candidate', () => {
57
+ expect(fuzzyMatch('xyz', candidates)).toBeNull()
58
+ })
59
+
60
+ it('returns null when candidates is empty', () => {
61
+ expect(fuzzyMatch('prospeo', [])).toBeNull()
62
+ })
63
+ })
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { resolvePreferredProviders } from '../../src/utils/provider-resolver.js'
3
+
4
+ describe('resolvePreferredProviders', () => {
5
+ describe('auto-route path (empty / absent)', () => {
6
+ it('returns ok with empty providers when field is absent', () => {
7
+ const r = resolvePreferredProviders('find_email', {}, undefined)
8
+ expect(r.ok).toBe(true)
9
+ if (r.ok) expect(r.providers).toEqual([])
10
+ })
11
+
12
+ it('returns ok with empty providers when field is empty array', () => {
13
+ const r = resolvePreferredProviders('find_email', {}, [])
14
+ expect(r.ok).toBe(true)
15
+ if (r.ok) expect(r.providers).toEqual([])
16
+ })
17
+ })
18
+
19
+ describe('Scenario A — valid provider, exact match', () => {
20
+ it('resolves known provider and returns it', () => {
21
+ const r = resolvePreferredProviders('find_email', { first_name: 'Michel', domain: 'coldiq.com' }, ['findymail'])
22
+ expect(r.ok).toBe(true)
23
+ if (r.ok) {
24
+ expect(r.providers).toEqual(['findymail'])
25
+ expect(r.matchedFrom).toBeUndefined()
26
+ }
27
+ })
28
+
29
+ it('preserves user order when multiple providers given', () => {
30
+ const r = resolvePreferredProviders(
31
+ 'find_email',
32
+ { first_name: 'Michel', domain: 'coldiq.com' },
33
+ ['prospeo', 'findymail'],
34
+ )
35
+ expect(r.ok).toBe(true)
36
+ if (r.ok) expect(r.providers).toEqual(['prospeo', 'findymail'])
37
+ })
38
+ })
39
+
40
+ describe('Scenario D — fuzzy match', () => {
41
+ it('resolves a 1-typo name and populates matchedFrom', () => {
42
+ const r = resolvePreferredProviders('find_email', { first_name: 'Michel', domain: 'coldiq.com' }, ['prospec'])
43
+ expect(r.ok).toBe(true)
44
+ if (r.ok) {
45
+ expect(r.providers).toEqual(['prospeo'])
46
+ expect(r.matchedFrom).toEqual({ prospec: 'prospeo' })
47
+ }
48
+ })
49
+
50
+ it('resolves case-insensitive name (normalized match)', () => {
51
+ const r = resolvePreferredProviders('find_email', { first_name: 'Michel', domain: 'coldiq.com' }, ['Prospeo'])
52
+ expect(r.ok).toBe(true)
53
+ if (r.ok) {
54
+ expect(r.providers).toEqual(['prospeo'])
55
+ expect(r.matchedFrom).toEqual({ Prospeo: 'prospeo' })
56
+ }
57
+ })
58
+ })
59
+
60
+ describe('Scenario E — unrecognized provider', () => {
61
+ it('returns error for completely unknown name', () => {
62
+ const r = resolvePreferredProviders('find_email', {}, ['salesforce'])
63
+ expect(r.ok).toBe(false)
64
+ if (!r.ok) {
65
+ expect(r.error.error).toContain("'salesforce' is not a recognized provider")
66
+ expect(r.error.error).toContain('ColdIQ will automatically pick the best tool')
67
+ expect(r.error.available_providers.length).toBeGreaterThan(0)
68
+ }
69
+ })
70
+
71
+ it('returns error for provider from a different capability', () => {
72
+ const r = resolvePreferredProviders('find_email', {}, ['google_maps'])
73
+ expect(r.ok).toBe(false)
74
+ if (!r.ok) {
75
+ expect(r.error.error).toContain("'google_maps' is not a recognized provider")
76
+ }
77
+ })
78
+ })
79
+
80
+ describe('Scenario C — gated provider', () => {
81
+ it('requires gate: message says "requires X, which wasn\'t provided"', () => {
82
+ // blitzapi for find_email requires linkedin_url
83
+ const r = resolvePreferredProviders('find_email', { first_name: 'Michel', domain: 'coldiq.com' }, ['blitzapi'])
84
+ expect(r.ok).toBe(false)
85
+ if (!r.ok) {
86
+ expect(r.error.error).toContain("'blitzapi' requires linkedin_url, which wasn't provided")
87
+ expect(r.error.error).toContain('ColdIQ will automatically pick the best tool')
88
+ }
89
+ })
90
+
91
+ it('incompatible_with gate: message says "is not compatible with X"', () => {
92
+ // apollo for search_companies is incompatible with year/tech/funding/revenue filters
93
+ const r = resolvePreferredProviders(
94
+ 'search_companies',
95
+ { keywords: ['SaaS'], min_founded_year: 2020 },
96
+ ['apollo'],
97
+ )
98
+ expect(r.ok).toBe(false)
99
+ if (!r.ok) {
100
+ expect(r.error.error).toContain("'apollo' is not compatible with")
101
+ expect(r.error.error).not.toContain("which wasn't provided")
102
+ expect(r.error.error).toContain('ColdIQ will automatically pick the best tool')
103
+ }
104
+ })
105
+
106
+ it('succeeds when gated provider inputs are satisfied', () => {
107
+ // blitzapi requires linkedin_url
108
+ const r = resolvePreferredProviders(
109
+ 'find_email',
110
+ { linkedin_url: 'https://www.linkedin.com/in/michel-lieben' },
111
+ ['blitzapi'],
112
+ )
113
+ expect(r.ok).toBe(true)
114
+ if (r.ok) expect(r.providers).toEqual(['blitzapi'])
115
+ })
116
+ })
117
+
118
+ describe('duplicate de-duplication', () => {
119
+ it('de-duplicates provider names that resolve to the same ID', () => {
120
+ const r = resolvePreferredProviders(
121
+ 'find_email',
122
+ { first_name: 'Michel', domain: 'coldiq.com' },
123
+ ['prospeo', 'Prospeo'],
124
+ )
125
+ expect(r.ok).toBe(true)
126
+ if (r.ok) {
127
+ expect(r.providers).toEqual(['prospeo'])
128
+ expect(r.providers).toHaveLength(1)
129
+ }
130
+ })
131
+ })
132
+
133
+ describe('find_emails capability (custom waterfall)', () => {
134
+ it('resolves providers for find_emails', () => {
135
+ const r = resolvePreferredProviders('find_emails', {}, ['prospeo'])
136
+ expect(r.ok).toBe(true)
137
+ if (r.ok) expect(r.providers).toEqual(['prospeo'])
138
+ })
139
+
140
+ it('errors for provider not in find_emails', () => {
141
+ const r = resolvePreferredProviders('find_emails', {}, ['wiza'])
142
+ expect(r.ok).toBe(false)
143
+ })
144
+ })
145
+ })