@coldiq/mcp 5.4.2 → 5.4.4

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.
@@ -172,24 +172,50 @@ describe('ai-ark-people', () => {
172
172
  expect(p().isApplicable!({ keywords: ['cold email'] })).toBe(true)
173
173
  })
174
174
 
175
- it('isApplicable: false with only company_domains', () => {
176
- expect(p().isApplicable!({ company_domains: ['coldiq.com'] })).toBe(false)
175
+ it('isApplicable: true with company identifiers or locations', () => {
176
+ expect(p().isApplicable!({ company_domains: ['coldiq.com'] })).toBe(true)
177
+ expect(p().isApplicable!({ company_linkedin_urls: ['https://www.linkedin.com/company/coldiq'] })).toBe(true)
178
+ expect(p().isApplicable!({ locations: ['Belgium'] })).toBe(true)
179
+ })
180
+
181
+ it('isApplicable: false for an unsupported seniority by itself', () => {
182
+ expect(p().isApplicable!({ seniorities: ['Supreme Leader'] })).toBe(false)
177
183
  })
178
184
 
179
- it('mapParams builds AI-Ark contact shape', () => {
185
+ it('mapParams builds the documented AI Ark account and contact shape', () => {
180
186
  expect(p().mapParams({
181
187
  job_titles: ['VP Sales'],
182
188
  seniorities: ['Director'],
183
189
  locations: ['Belgium'],
184
190
  keywords: ['outbound'],
191
+ company_domains: ['coldiq.com'],
192
+ company_linkedin_urls: ['https://www.linkedin.com/company/coldiq'],
185
193
  limit: 20,
186
194
  })).toEqual({
187
195
  body: {
196
+ account: {
197
+ domain: { any: { include: ['coldiq.com'] } },
198
+ linkedin: { any: { include: ['https://www.linkedin.com/company/coldiq'] } },
199
+ },
188
200
  contact: {
189
- departmentAndFunction: { any: { include: ['VP Sales'] } },
190
- seniority: { any: { include: ['Director'] } },
201
+ experience: {
202
+ latest: {
203
+ title: { any: { include: { mode: 'SMART', content: ['VP Sales'] } } },
204
+ },
205
+ },
206
+ seniority: { any: { include: ['director'] } },
191
207
  location: { any: { include: ['Belgium'] } },
192
- keywordsInProfile: { any: { include: ['outbound'] } },
208
+ keyword: {
209
+ any: {
210
+ include: {
211
+ sources: [
212
+ { mode: 'SMART', source: 'HEADLINE' },
213
+ { mode: 'SMART', source: 'SUMMARY' },
214
+ ],
215
+ content: ['outbound'],
216
+ },
217
+ },
218
+ },
193
219
  },
194
220
  size: 20,
195
221
  },
@@ -201,7 +227,7 @@ describe('ai-ark-people', () => {
201
227
  const contact = (result.body as Record<string, unknown>).contact as Record<string, unknown>
202
228
  expect(contact.seniority).toBeUndefined()
203
229
  expect(contact.location).toBeUndefined()
204
- expect(contact.keywordsInProfile).toBeUndefined()
230
+ expect(contact.keyword).toBeUndefined()
205
231
  })
206
232
 
207
233
  it('mapParams caps size at 100', () => {
@@ -103,15 +103,31 @@ describe('ai-ark-companies', () => {
103
103
  expect(p().isApplicable!({ countries: ['BE'] })).toBe(true)
104
104
  })
105
105
 
106
- it('isApplicable: false with only min_employees', () => {
107
- expect(p().isApplicable!({ min_employees: 50 })).toBe(false)
106
+ it('isApplicable: true with employee range, valid funding, or lookalike domains', () => {
107
+ expect(p().isApplicable!({ min_employees: 50 })).toBe(true)
108
+ expect(p().isApplicable!({ funding_stages: ['Series A'] })).toBe(true)
109
+ expect(p().isApplicable!({ similar_to_domains: ['coldiq.com'] })).toBe(true)
110
+ })
111
+
112
+ it('isApplicable: false for an unsupported funding stage by itself', () => {
113
+ expect(p().isApplicable!({ funding_stages: ['not-a-real-stage'] })).toBe(false)
114
+ })
115
+
116
+ it('isApplicable: false when lookalike input exceeds AI Ark\'s five-domain limit', () => {
117
+ const similarToDomains = ['a.com', 'b.com', 'c.com', 'd.com', 'e.com', 'f.com']
118
+ expect(p().isApplicable!({
119
+ keywords: ['outbound sales'],
120
+ similar_to_domains: similarToDomains,
121
+ })).toBe(false)
122
+ expect(() => p().mapParams({ similar_to_domains: similarToDomains })).toThrow()
108
123
  })
109
124
 
110
- it('mapParams builds AI-Ark account/contact shape', () => {
125
+ it('mapParams builds the documented AI Ark company filter shape', () => {
111
126
  const result = p().mapParams({
112
127
  keywords: ['outbound sales'],
113
128
  industries: ['SaaS'],
114
129
  countries: ['BE'],
130
+ similar_to_domains: ['coldiq.com'],
115
131
  min_employees: 10,
116
132
  max_employees: 500,
117
133
  funding_stages: ['Series A'],
@@ -119,17 +135,28 @@ describe('ai-ark-companies', () => {
119
135
  })
120
136
  expect(result).toEqual({
121
137
  body: {
138
+ lookalikeDomains: ['coldiq.com'],
122
139
  account: {
123
- industries: { any: { include: ['SaaS'] } },
140
+ industries: { any: { include: { mode: 'WORD', content: ['SaaS'] } } },
141
+ keyword: {
142
+ any: {
143
+ include: {
144
+ sources: [
145
+ { mode: 'SMART', source: 'NAME' },
146
+ { mode: 'SMART', source: 'KEYWORD' },
147
+ { mode: 'SMART', source: 'DESCRIPTION' },
148
+ { mode: 'SMART', source: 'SEO' },
149
+ ],
150
+ content: ['outbound sales'],
151
+ },
152
+ },
153
+ },
124
154
  location: { any: { include: ['Belgium'] } },
125
155
  employeeSize: {
126
156
  type: 'RANGE',
127
157
  range: [{ start: 10, end: 500 }],
128
158
  },
129
- funding: { stage: { include: ['Series A'] } },
130
- },
131
- contact: {
132
- keywordsInProfile: { any: { include: ['outbound sales'] } },
159
+ funding: { type: ['SERIES_A'] },
133
160
  },
134
161
  size: 20,
135
162
  },
@@ -145,7 +172,7 @@ describe('ai-ark-companies', () => {
145
172
  expect(account.funding).toBeUndefined()
146
173
  })
147
174
 
148
- it('mapParams omits contact when no keywords', () => {
175
+ it('mapParams never creates a contact filter for company keywords', () => {
149
176
  const result = p().mapParams({ industries: ['SaaS'] })
150
177
  expect((result.body as Record<string, unknown>).contact).toBeUndefined()
151
178
  })
@@ -223,7 +223,7 @@ describe('registry', () => {
223
223
  })
224
224
 
225
225
  it('BlitzAPI folds industries into keywords (industry filter requires LinkedIn taxonomy)', () => {
226
- const providers = getProviders('search_companies')
226
+ const providers = getConfiguredProviders('search_companies')
227
227
  const blitz = providers.find((p) => p.id === 'blitzapi')!
228
228
  const result = blitz.mapParams({
229
229
  keywords: ['AI'],
@@ -246,7 +246,7 @@ describe('registry', () => {
246
246
  })
247
247
 
248
248
  it('BlitzAPI isApplicable rejects empty input but accepts basic narrowing filters', () => {
249
- const providers = getProviders('search_companies')
249
+ const providers = getConfiguredProviders('search_companies')
250
250
  const blitz = providers.find((p) => p.id === 'blitzapi')!
251
251
  expect(blitz.isApplicable!({})).toBe(false)
252
252
  expect(blitz.isApplicable!({ keywords: ['AI'] })).toBe(true)
@@ -255,7 +255,7 @@ describe('registry', () => {
255
255
  })
256
256
 
257
257
  it('BlitzAPI isApplicable rejects advanced filters it cannot apply', () => {
258
- const providers = getProviders('search_companies')
258
+ const providers = getConfiguredProviders('search_companies')
259
259
  const blitz = providers.find((p) => p.id === 'blitzapi')!
260
260
  expect(blitz.isApplicable!({ technologies: ['Salesforce'] })).toBe(false)
261
261
  expect(blitz.isApplicable!({ funding_stages: ['series_a'] })).toBe(false)
@@ -271,7 +271,7 @@ describe('registry', () => {
271
271
  })
272
272
 
273
273
  it('BlitzAPI postFilter enforces employee count when present, passes through when missing', () => {
274
- const providers = getProviders('search_companies')
274
+ const providers = getConfiguredProviders('search_companies')
275
275
  const blitz = providers.find((p) => p.id === 'blitzapi')!
276
276
  const results = [
277
277
  { name: 'A', employees_on_linkedin: 30 },
@@ -284,7 +284,7 @@ describe('registry', () => {
284
284
  })
285
285
 
286
286
  it('BlitzAPI postFilter is a no-op when no employee filters are set', () => {
287
- const providers = getProviders('search_companies')
287
+ const providers = getConfiguredProviders('search_companies')
288
288
  const blitz = providers.find((p) => p.id === 'blitzapi')!
289
289
  const results = [{ name: 'A', employees_on_linkedin: 30 }]
290
290
  const out = blitz.postFilter!({ results }, {}) as Record<string, unknown>
@@ -292,7 +292,7 @@ describe('registry', () => {
292
292
  })
293
293
 
294
294
  it('BlitzAPI caps max_results at 50', () => {
295
- const providers = getProviders('search_companies')
295
+ const providers = getConfiguredProviders('search_companies')
296
296
  const blitz = providers.find((p) => p.id === 'blitzapi')!
297
297
  const result = blitz.mapParams({ keywords: ['AI'], limit: 100 })
298
298
  expect((result.body as { max_results: number }).max_results).toBe(50)
@@ -472,7 +472,6 @@ describe('registry', () => {
472
472
  'fullenrich',
473
473
  'theirstack',
474
474
  'signalbase',
475
- 'blitzapi',
476
475
  'apollo',
477
476
  'limadata',
478
477
  'predictleads',
@@ -800,7 +799,7 @@ describe('registry', () => {
800
799
  const ids = getProviders('enrich_company').map((p) => p.id)
801
800
  expect(ids).toEqual([
802
801
  'apollo', 'limadata', 'prospeo',
803
- 'blitzapi', 'wiza', 'findymail', 'icypeas',
802
+ 'wiza', 'findymail', 'icypeas',
804
803
  'builtwith', 'openmart', 'linkupapi-by-domain', 'linkupapi-by-url', 'discolike',
805
804
  ])
806
805
  })
@@ -890,7 +889,7 @@ describe('registry', () => {
890
889
  })
891
890
 
892
891
  it('BlitzAPI is gated on linkedin_url and passes company_linkedin_url', () => {
893
- const bz = getProviders('enrich_company').find((p) => p.id === 'blitzapi')!
892
+ const bz = getConfiguredProviders('enrich_company').find((p) => p.id === 'blitzapi')!
894
893
  expect(bz.isApplicable!({ linkedin_url: 'https://linkedin.com/company/stripe' })).toBe(true)
895
894
  expect(bz.isApplicable!({ domain: 'stripe.com' })).toBe(false)
896
895
  expect(bz.mapParams({ linkedin_url: 'https://linkedin.com/company/stripe' }).body).toEqual({ company_linkedin_url: 'https://linkedin.com/company/stripe' })
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { initClient } from '../../src/client.js'
3
+ import { getWebsiteVisitorsHandler, getWebsiteVisitorsSchema } from '../../src/tools/get-website-visitors.js'
4
+
5
+ describe('visitor-ID MCP tools', () => {
6
+ const originalFetch = globalThis.fetch
7
+
8
+ beforeEach(() => {
9
+ initClient('http://test-api.local', 'test-key')
10
+ })
11
+
12
+ afterEach(() => {
13
+ globalThis.fetch = originalFetch
14
+ vi.restoreAllMocks()
15
+ })
16
+
17
+ function captureUrl(payload: unknown, status = 200) {
18
+ const seen: { url: string } = { url: '' }
19
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
20
+ seen.url = url.toString()
21
+ return new Response(JSON.stringify(payload), { status })
22
+ }) as typeof fetch
23
+ return seen
24
+ }
25
+
26
+ describe('get_website_visitors', () => {
27
+ it('reads identified people from the contacts endpoint', async () => {
28
+ const seen = captureUrl({ data: [{ email: 'michel@coldiq.com' }], next_cursor: 'abc' })
29
+
30
+ const result = await getWebsiteVisitorsHandler({ website_id: 7, type: 'people', limit: 25 })
31
+
32
+ const url = new URL(seen.url)
33
+ expect(url.pathname).toBe('/v1/visitor-id/websites/7/contacts')
34
+ expect(url.searchParams.get('limit')).toBe('25')
35
+ expect(result.isError).toBeFalsy()
36
+ })
37
+
38
+ it('reads identified companies from the accounts endpoint', async () => {
39
+ const seen = captureUrl({ data: [{ domain: 'coldiq.com' }] })
40
+
41
+ await getWebsiteVisitorsHandler({ website_id: 7, type: 'companies', domain: 'coldiq.com' })
42
+
43
+ const url = new URL(seen.url)
44
+ expect(url.pathname).toBe('/v1/visitor-id/websites/7/accounts')
45
+ expect(url.searchParams.get('domain')).toBe('coldiq.com')
46
+ })
47
+
48
+ it('sends people-only filters only for people, and company-only filters only for companies', async () => {
49
+ const people = captureUrl({ data: [] })
50
+ await getWebsiteVisitorsHandler({
51
+ website_id: 7,
52
+ type: 'people',
53
+ email: 'michel@coldiq.com',
54
+ account_id: 'acc_1',
55
+ domain: 'coldiq.com',
56
+ })
57
+ const peopleUrl = new URL(people.url)
58
+ expect(peopleUrl.searchParams.get('email')).toBe('michel@coldiq.com')
59
+ expect(peopleUrl.searchParams.get('account_id')).toBe('acc_1')
60
+ expect(peopleUrl.searchParams.get('domain')).toBeNull()
61
+
62
+ const companies = captureUrl({ data: [] })
63
+ await getWebsiteVisitorsHandler({
64
+ website_id: 7,
65
+ type: 'companies',
66
+ domain: 'coldiq.com',
67
+ email: 'michel@coldiq.com',
68
+ })
69
+ const companiesUrl = new URL(companies.url)
70
+ expect(companiesUrl.searchParams.get('domain')).toBe('coldiq.com')
71
+ expect(companiesUrl.searchParams.get('email')).toBeNull()
72
+ })
73
+
74
+ it('forwards the pagination cursor', async () => {
75
+ const seen = captureUrl({ data: [] })
76
+
77
+ await getWebsiteVisitorsHandler({ website_id: 7, type: 'people', cursor: 'next-page' })
78
+
79
+ expect(new URL(seen.url).searchParams.get('cursor')).toBe('next-page')
80
+ })
81
+
82
+ it('passes the upstream cursor envelope through verbatim', async () => {
83
+ captureUrl({ data: [{ id: 'c1' }], next_cursor: 'page2', has_more: true })
84
+
85
+ const result = await getWebsiteVisitorsHandler({ website_id: 7, type: 'people' })
86
+ const parsed = JSON.parse(result.content[0].text)
87
+
88
+ expect(parsed.data).toEqual({ data: [{ id: 'c1' }], next_cursor: 'page2', has_more: true })
89
+ })
90
+
91
+ it('returns isError when the website is not found', async () => {
92
+ captureUrl({ error: 'Website not found' }, 404)
93
+
94
+ const result = await getWebsiteVisitorsHandler({ website_id: 999, type: 'companies' })
95
+
96
+ expect(result.isError).toBe(true)
97
+ const parsed = JSON.parse(result.content[0].text)
98
+ expect(parsed.status).toBe(404)
99
+ expect(parsed.error).toBe('Failed to read identified companies')
100
+ })
101
+
102
+ it('rejects a type outside people/companies at the schema layer', () => {
103
+ expect(getWebsiteVisitorsSchema.type.safeParse('visitors').success).toBe(false)
104
+ expect(getWebsiteVisitorsSchema.type.safeParse('people').success).toBe(true)
105
+ expect(getWebsiteVisitorsSchema.limit.safeParse(101).success).toBe(false)
106
+ })
107
+ })
108
+ })
@@ -1,14 +1,27 @@
1
1
  import { describe, it, expect } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
2
4
  import { readPackageVersion } from '../src/version.js'
3
5
 
4
6
  describe('readPackageVersion', () => {
5
7
  it('reports the real package.json version relative to the caller URL', () => {
6
8
  // This test file sits at mcp/tests/, so '../package.json' from here is mcp/package.json.
7
9
  const version = readPackageVersion(import.meta.url)
8
- expect(version).toMatch(/^\d+\.\d+\.\d+/)
10
+ expect(version).toMatch(/^5\.4\.\d+$/)
9
11
  expect(version).not.toBe('0.1.0') // the old hardcoded placeholder
10
12
  })
11
13
 
14
+ it('keeps package.json and package-lock.json on the same 5.4.x version', () => {
15
+ const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url))
16
+ const packageLockPath = fileURLToPath(new URL('../package-lock.json', import.meta.url))
17
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
18
+ const packageLock = JSON.parse(readFileSync(packageLockPath, 'utf8'))
19
+
20
+ expect(packageJson.version).toMatch(/^5\.4\.\d+$/)
21
+ expect(packageLock.version).toBe(packageJson.version)
22
+ expect(packageLock.packages[''].version).toBe(packageJson.version)
23
+ })
24
+
12
25
  it('falls back to 0.0.0 instead of throwing when package.json cannot be resolved', () => {
13
26
  const version = readPackageVersion('file:///nonexistent/deeply/nested/module.js')
14
27
  expect(version).toBe('0.0.0')