@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.
- package/dist/ai-ark-mapping.d.ts +97 -0
- package/dist/ai-ark-mapping.d.ts.map +1 -0
- package/dist/ai-ark-mapping.js +162 -0
- package/dist/ai-ark-mapping.js.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/provider-availability.d.ts.map +1 -1
- package/dist/provider-availability.js +4 -0
- package/dist/provider-availability.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -52
- package/dist/registry.js.map +1 -1
- package/dist/tools/get-website-visitors.d.ts +26 -0
- package/dist/tools/get-website-visitors.d.ts.map +1 -0
- package/dist/tools/get-website-visitors.js +73 -0
- package/dist/tools/get-website-visitors.js.map +1 -0
- package/dist/utils/provider-resolver.js +2 -2
- package/dist/utils/provider-resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/ai-ark-mapping.ts +175 -0
- package/src/index.ts +11 -0
- package/src/provider-availability.ts +4 -0
- package/src/registry.ts +10 -54
- package/src/tools/get-website-visitors.ts +74 -0
- package/src/utils/provider-resolver.ts +2 -2
- package/tests/provider-availability.test.ts +21 -0
- package/tests/registry-find-people.test.ts +33 -7
- package/tests/registry-search-companies.test.ts +36 -9
- package/tests/registry.test.ts +8 -9
- package/tests/tools/visitor-id-tools.test.ts +108 -0
- package/tests/version.test.ts +14 -1
|
@@ -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:
|
|
176
|
-
expect(p().isApplicable!({ company_domains: ['coldiq.com'] })).toBe(
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
107
|
-
expect(p().isApplicable!({ min_employees: 50 })).toBe(
|
|
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
|
|
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: {
|
|
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
|
|
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
|
})
|
package/tests/registry.test.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
'
|
|
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 =
|
|
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
|
+
})
|
package/tests/version.test.ts
CHANGED
|
@@ -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(
|
|
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')
|