@coldiq/mcp 0.1.9 → 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 (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
@@ -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
+ })