@coldiq/mcp 5.4.3 → 5.4.5

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 (87) hide show
  1. package/dist/ai-ark-mapping.d.ts +97 -0
  2. package/dist/ai-ark-mapping.d.ts.map +1 -0
  3. package/dist/ai-ark-mapping.js +162 -0
  4. package/dist/ai-ark-mapping.js.map +1 -0
  5. package/dist/provider-availability.d.ts.map +1 -1
  6. package/dist/provider-availability.js +4 -0
  7. package/dist/provider-availability.js.map +1 -1
  8. package/dist/registry.d.ts.map +1 -1
  9. package/dist/registry.js +5 -52
  10. package/dist/registry.js.map +1 -1
  11. package/dist/tool-json-schema.d.ts +8 -0
  12. package/dist/tool-json-schema.d.ts.map +1 -0
  13. package/dist/tool-json-schema.js +19 -0
  14. package/dist/tool-json-schema.js.map +1 -0
  15. package/dist/tools/call-endpoint.d.ts +7 -1
  16. package/dist/tools/call-endpoint.d.ts.map +1 -1
  17. package/dist/tools/call-endpoint.js +1 -1
  18. package/dist/tools/call-endpoint.js.map +1 -1
  19. package/dist/tools/enrich-company-bulk.d.ts +1 -9
  20. package/dist/tools/enrich-company-bulk.d.ts.map +1 -1
  21. package/dist/tools/enrich-company.d.ts +1 -1
  22. package/dist/tools/enrich-person-bulk.d.ts +1 -15
  23. package/dist/tools/enrich-person-bulk.d.ts.map +1 -1
  24. package/dist/tools/enrich-person.d.ts +1 -1
  25. package/dist/tools/extract-post-engagement.d.ts +5 -1
  26. package/dist/tools/extract-post-engagement.d.ts.map +1 -1
  27. package/dist/tools/fetch-page-content.d.ts +2 -2
  28. package/dist/tools/find-email.d.ts +1 -1
  29. package/dist/tools/find-emails-bulk.d.ts +1 -15
  30. package/dist/tools/find-emails-bulk.d.ts.map +1 -1
  31. package/dist/tools/find-emails.d.ts +2 -14
  32. package/dist/tools/find-emails.d.ts.map +1 -1
  33. package/dist/tools/find-influencers.d.ts +28 -8
  34. package/dist/tools/find-influencers.d.ts.map +1 -1
  35. package/dist/tools/find-influencers.js +1 -1
  36. package/dist/tools/find-influencers.js.map +1 -1
  37. package/dist/tools/find-people.d.ts +11 -8
  38. package/dist/tools/find-people.d.ts.map +1 -1
  39. package/dist/tools/find-phone-bulk.d.ts +1 -13
  40. package/dist/tools/find-phone-bulk.d.ts.map +1 -1
  41. package/dist/tools/find-phone.d.ts +1 -1
  42. package/dist/tools/find-signals.d.ts +16 -8
  43. package/dist/tools/find-signals.d.ts.map +1 -1
  44. package/dist/tools/get-endpoint-details.d.ts +7 -1
  45. package/dist/tools/get-endpoint-details.d.ts.map +1 -1
  46. package/dist/tools/get-place-reviews.d.ts +8 -3
  47. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  48. package/dist/tools/get-website-visitors.d.ts +4 -1
  49. package/dist/tools/get-website-visitors.d.ts.map +1 -1
  50. package/dist/tools/search-ads.d.ts +14 -6
  51. package/dist/tools/search-ads.d.ts.map +1 -1
  52. package/dist/tools/search-companies.d.ts +15 -12
  53. package/dist/tools/search-companies.d.ts.map +1 -1
  54. package/dist/tools/search-jobs.d.ts +54 -22
  55. package/dist/tools/search-jobs.d.ts.map +1 -1
  56. package/dist/tools/search-knowledge.d.ts +6 -2
  57. package/dist/tools/search-knowledge.d.ts.map +1 -1
  58. package/dist/tools/search-knowledge.js +1 -1
  59. package/dist/tools/search-knowledge.js.map +1 -1
  60. package/dist/tools/search-places.d.ts +16 -8
  61. package/dist/tools/search-places.d.ts.map +1 -1
  62. package/dist/tools/search-reddit.d.ts +24 -5
  63. package/dist/tools/search-reddit.d.ts.map +1 -1
  64. package/dist/tools/search-seo.d.ts +33 -8
  65. package/dist/tools/search-seo.d.ts.map +1 -1
  66. package/dist/tools/search-web.d.ts +7 -2
  67. package/dist/tools/search-web.d.ts.map +1 -1
  68. package/dist/tools/verify-email.d.ts +1 -1
  69. package/dist/tools/verify-emails-bulk.d.ts +2 -2
  70. package/dist/utils/provider-resolver.js +2 -2
  71. package/dist/utils/provider-resolver.js.map +1 -1
  72. package/package.json +6 -4
  73. package/src/ai-ark-mapping.ts +175 -0
  74. package/src/provider-availability.ts +4 -0
  75. package/src/registry.ts +10 -54
  76. package/src/tool-json-schema.ts +21 -0
  77. package/src/tools/call-endpoint.ts +1 -1
  78. package/src/tools/find-influencers.ts +1 -1
  79. package/src/tools/search-knowledge.ts +1 -1
  80. package/src/utils/provider-resolver.ts +2 -2
  81. package/tests/gtm/harness.ts +2 -2
  82. package/tests/provider-availability.test.ts +21 -0
  83. package/tests/registry-find-people.test.ts +33 -7
  84. package/tests/registry-search-companies.test.ts +36 -9
  85. package/tests/registry.test.ts +8 -9
  86. package/tests/tool-json-schema.test.ts +93 -0
  87. package/tests/version.test.ts +14 -1
@@ -6,11 +6,15 @@
6
6
  * configured and testable while public routing and tool schemas omit them.
7
7
  */
8
8
  const DISABLED_PROVIDERS = new Set([
9
+ 'blitzapi',
9
10
  'companyenrich',
10
11
  'people-data-labs',
11
12
  ])
12
13
 
13
14
  const PROVIDER_ALIASES: Record<string, string> = {
15
+ blitz: 'blitzapi',
16
+ 'blitz-api': 'blitzapi',
17
+ 'blitzapi-reverse-email': 'blitzapi',
14
18
  'company-enrich': 'companyenrich',
15
19
  'companyenrich-props': 'companyenrich',
16
20
  pdl: 'people-data-labs',
package/src/registry.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
5
  import { isProviderEnabled } from './provider-availability.js'
6
+ import {
7
+ isAiArkCompanySearchApplicable,
8
+ isAiArkPeopleSearchApplicable,
9
+ mapAiArkCompanySearchParams,
10
+ mapAiArkPeopleSearchParams,
11
+ } from './ai-ark-mapping.js'
6
12
 
7
13
  export interface AsyncConfig {
8
14
  /** Build the poll endpoint path from the ID returned by the create call */
@@ -1069,37 +1075,8 @@ const searchCompaniesProviders: ProviderEntry[] = [
1069
1075
  endpoint: '/ai-ark/companies',
1070
1076
  method: 'POST',
1071
1077
  priority: 18,
1072
- isApplicable: (input) =>
1073
- isNonEmptyArray(input.keywords) ||
1074
- isNonEmptyArray(input.industries) ||
1075
- isNonEmptyArray(input.countries),
1076
- mapParams: (input) => ({
1077
- body: {
1078
- account: {
1079
- ...(isNonEmptyArray(input.industries) && {
1080
- industries: { any: { include: input.industries } },
1081
- }),
1082
- ...(isNonEmptyArray(input.countries) && {
1083
- location: { any: { include: (input.countries as string[]).map(isoCountryToName) } },
1084
- }),
1085
- ...((input.min_employees !== undefined || input.max_employees !== undefined) && {
1086
- employeeSize: {
1087
- type: 'RANGE',
1088
- range: [{ start: input.min_employees, end: input.max_employees }],
1089
- },
1090
- }),
1091
- ...(isNonEmptyArray(input.funding_stages) && {
1092
- funding: { stage: { include: input.funding_stages } },
1093
- }),
1094
- },
1095
- ...(isNonEmptyArray(input.keywords) && {
1096
- contact: {
1097
- keywordsInProfile: { any: { include: input.keywords } },
1098
- },
1099
- }),
1100
- size: Math.min((input.limit as number | undefined) ?? 10, 100),
1101
- },
1102
- }),
1078
+ isApplicable: isAiArkCompanySearchApplicable,
1079
+ mapParams: (input) => mapAiArkCompanySearchParams(input, isoCountryToName),
1103
1080
  hasResult: (data) => {
1104
1081
  const d = data as Record<string, unknown>
1105
1082
  return isNonEmptyArray(d.content)
@@ -1518,29 +1495,8 @@ const findPeopleProviders: ProviderEntry[] = [
1518
1495
  endpoint: '/ai-ark/people',
1519
1496
  method: 'POST',
1520
1497
  priority: 8,
1521
- isApplicable: (input) =>
1522
- isNonEmptyArray(input.job_titles) ||
1523
- isNonEmptyArray(input.seniorities) ||
1524
- isNonEmptyArray(input.keywords),
1525
- mapParams: (input) => ({
1526
- body: {
1527
- contact: {
1528
- ...(isNonEmptyArray(input.job_titles) && {
1529
- departmentAndFunction: { any: { include: input.job_titles } },
1530
- }),
1531
- ...(isNonEmptyArray(input.seniorities) && {
1532
- seniority: { any: { include: input.seniorities } },
1533
- }),
1534
- ...(isNonEmptyArray(input.locations) && {
1535
- location: { any: { include: input.locations } },
1536
- }),
1537
- ...(isNonEmptyArray(input.keywords) && {
1538
- keywordsInProfile: { any: { include: input.keywords } },
1539
- }),
1540
- },
1541
- size: Math.min((input.limit as number | undefined) ?? 10, 100),
1542
- },
1543
- }),
1498
+ isApplicable: isAiArkPeopleSearchApplicable,
1499
+ mapParams: mapAiArkPeopleSearchParams,
1544
1500
  hasResult: (data) => {
1545
1501
  const d = data as Record<string, unknown>
1546
1502
  return isNonEmptyArray(d.content)
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod'
2
+
3
+ const ToolInputJsonSchema = z.object({
4
+ type: z.literal('object'),
5
+ properties: z.json().optional(),
6
+ }).passthrough()
7
+
8
+ /** Convert a tool input schema without widening Zod object contracts. */
9
+ export function toOpenApiInputSchema(schema: z.ZodObject) {
10
+ const generated = z.toJSONSchema(schema, {
11
+ target: 'openapi-3.0',
12
+ io: 'input',
13
+ override: ({ zodSchema, jsonSchema }) => {
14
+ if (zodSchema instanceof z.ZodObject && jsonSchema.additionalProperties === undefined) {
15
+ jsonSchema.additionalProperties = false
16
+ }
17
+ },
18
+ })
19
+
20
+ return ToolInputJsonSchema.parse(generated)
21
+ }
@@ -16,7 +16,7 @@ export const callEndpointSchema = {
16
16
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional()
17
17
  .describe('HTTP method from search_endpoints. Set this when one path supports more than one method.'),
18
18
  params: z
19
- .record(z.unknown())
19
+ .record(z.string(), z.unknown())
20
20
  .optional()
21
21
  .describe('Provider-native request params (request body fields, plus any path/query params), as shown in get_endpoint_details.'),
22
22
  }
@@ -13,7 +13,7 @@ export const findInfluencersSchema = {
13
13
  .describe('Target platform.'),
14
14
  limit: z.number().int().min(1).max(100).default(25)
15
15
  .describe('Max creators to return (1–100). Pricing is returned in the response credit headers.'),
16
- page: z.number().int().min(1).default(1).optional()
16
+ page: z.number().int().min(1).optional().meta({ default: 1 })
17
17
  .describe('Page number for pagination.'),
18
18
  sort_by: z.enum(['relevancy', 'number_of_followers', 'engagement_rate']).optional(),
19
19
  sort_order: z.enum(['asc', 'desc']).optional(),
@@ -26,8 +26,8 @@ export const searchKnowledgeSchema = {
26
26
  .int()
27
27
  .min(1)
28
28
  .max(15)
29
- .default(5)
30
29
  .optional()
30
+ .meta({ default: 5 })
31
31
  .describe('Maximum ranked documents to return (default 5, max 15).'),
32
32
  }
33
33
 
@@ -202,14 +202,14 @@ const GATED_DESCRIPTIONS: Partial<Record<Capability, Partial<Record<string, Gate
202
202
  'linkupapi-fundraising': { kind: 'requires', fields: 'funding filters and keywords or industries' },
203
203
  'linkupapi-hiring': { kind: 'requires', fields: 'is_hiring: true' },
204
204
  'prospeo-search-company': { kind: 'requires', fields: 'keywords, industries, or countries' },
205
- 'ai-ark-companies': { kind: 'requires', fields: 'keywords, industries, or countries' },
205
+ 'ai-ark-companies': { kind: 'requires', fields: 'keywords, industries, countries, similar_to_domains, funding_stages, or employee range' },
206
206
  },
207
207
  // -------------------------------------------------------------------------
208
208
  find_people: {
209
209
  'linkupapi-search-profiles': { kind: 'incompatible_with', fields: 'company_linkedin_urls or company_domains (this provider only works without company filters)' },
210
210
  'sumble-people-find': { kind: 'requires', fields: 'company_domains or company_linkedin_urls, and job_titles or seniorities' },
211
211
  'prospeo-search-person': { kind: 'requires', fields: 'job_titles or company_domains' },
212
- 'ai-ark-people': { kind: 'requires', fields: 'job_titles, seniorities, or keywords' },
212
+ 'ai-ark-people': { kind: 'requires', fields: 'job_titles, seniorities, keywords, locations, or company identifiers' },
213
213
  'findymail-search-employees': { kind: 'requires', fields: 'company_domains and job_titles' },
214
214
  'fullenrich-people-search': { kind: 'requires', fields: 'company_domains (upstream does not accept LinkedIn URL filters)' },
215
215
  pdl: { kind: 'requires', fields: 'company_domains, company_linkedin_urls, job_titles, or seniorities' },
@@ -1,10 +1,10 @@
1
1
  import Anthropic from '@anthropic-ai/sdk'
2
- import { zodToJsonSchema } from 'zod-to-json-schema'
3
2
  import * as fs from 'node:fs'
4
3
  import * as path from 'node:path'
5
4
  import * as url from 'node:url'
6
5
 
7
6
  import { MODEL_PROFILES } from '../../../src/agent/model-policy.js'
7
+ import { toOpenApiInputSchema } from '../../src/tool-json-schema.js'
8
8
  import { MCP_TOOLS } from './tools-bridge.js'
9
9
 
10
10
  export interface ToolCall {
@@ -49,7 +49,7 @@ export async function runAgent(options: RunAgentOptions): Promise<AgentResult> {
49
49
  const anthropicTools: Anthropic.Tool[] = MCP_TOOLS.map((t) => ({
50
50
  name: t.name,
51
51
  description: t.description,
52
- input_schema: zodToJsonSchema(t.schema, { target: 'openApi3' }) as Anthropic.Tool['input_schema'],
52
+ input_schema: toOpenApiInputSchema(t.schema),
53
53
  }))
54
54
 
55
55
  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: prompt }]
@@ -2,10 +2,13 @@ import { describe, expect, it } from 'vitest'
2
2
  import { isProviderEnabled as isWorkerProviderEnabled } from '../../src/lib/provider-availability.js'
3
3
  import { isProviderEnabled } from '../src/provider-availability.js'
4
4
  import { getConfiguredProviders, getProviders, type Capability } from '../src/registry.js'
5
+ import { enrichCompanySchema } from '../src/tools/enrich-company.js'
6
+ import { enrichPersonSchema } from '../src/tools/enrich-person.js'
5
7
  import { findEmailSchema } from '../src/tools/find-email.js'
6
8
  import { findEmailsSchema } from '../src/tools/find-emails.js'
7
9
  import { findEmailsBulkSchema } from '../src/tools/find-emails-bulk.js'
8
10
  import { getEndpointDetailsSchema } from '../src/tools/get-endpoint-details.js'
11
+ import { searchCompaniesSchema } from '../src/tools/search-companies.js'
9
12
  import { listDataSourcesHandler } from '../src/tools/list-data-sources.js'
10
13
  import {
11
14
  FIND_EMAILS_PROVIDERS,
@@ -36,6 +39,12 @@ const CAPABILITIES: Capability[] = [
36
39
  describe('standalone MCP provider availability', () => {
37
40
  it('mirrors every disabled-provider alias from the Worker policy', () => {
38
41
  for (const alias of [
42
+ 'blitzapi',
43
+ 'BlitzAPI',
44
+ 'Blitz API',
45
+ 'Blitz',
46
+ 'blitzapi-reverse-email',
47
+ 'blitzapi_reverse_email',
39
48
  'companyenrich',
40
49
  'CompanyEnrich',
41
50
  'Company Enrich',
@@ -58,12 +67,17 @@ describe('standalone MCP provider availability', () => {
58
67
  expect(getConfiguredProviders('find_people').some((provider) => provider.id === 'pdl')).toBe(true)
59
68
  expect(getConfiguredProviders('search_companies').some((provider) => provider.id === 'companyenrich')).toBe(true)
60
69
  expect(getConfiguredProviders('enrich_company').some((provider) => provider.id === 'companyenrich_props')).toBe(true)
70
+ expect(getConfiguredProviders('search_companies').some((provider) => provider.id === 'blitzapi')).toBe(true)
71
+ expect(getConfiguredProviders('enrich_company').some((provider) => provider.id === 'blitzapi')).toBe(true)
72
+ expect(getConfiguredProviders('enrich_person').some((provider) => provider.id === 'blitzapi-reverse-email')).toBe(true)
61
73
 
62
74
  for (const capability of CAPABILITIES) {
63
75
  expect(getProviders(capability).every((provider) => isProviderEnabled(provider.id))).toBe(true)
64
76
  }
65
77
  expect(FIND_EMAILS_PROVIDERS.every(isProviderEnabled)).toBe(true)
66
78
  expect(getProvidersForCapability('find_people')).not.toContain('pdl')
79
+ expect(getProvidersForCapability('search_companies')).not.toContain('blitzapi')
80
+ expect(getProvidersForCapability('enrich_person')).not.toContain('blitzapi-reverse-email')
67
81
  })
68
82
 
69
83
  it('rejects an explicit disabled-provider pin without advertising it as available', () => {
@@ -72,6 +86,9 @@ describe('standalone MCP provider availability', () => {
72
86
  ['find_emails', 'pdl-person-enrich'],
73
87
  ['search_companies', 'companyenrich'],
74
88
  ['enrich_company', 'companyenrich_props'],
89
+ ['search_companies', 'blitzapi'],
90
+ ['enrich_company', 'blitzapi'],
91
+ ['enrich_person', 'blitzapi-reverse-email'],
75
92
  ] as const) {
76
93
  const result = resolvePreferredProviders(capability, {}, [provider])
77
94
  expect(result.ok).toBe(false)
@@ -92,11 +109,15 @@ describe('standalone MCP provider availability', () => {
92
109
  findEmailsSchema.max_credits.description,
93
110
  findEmailsBulkSchema.max_credits.description,
94
111
  getEndpointDetailsSchema.api.description,
112
+ searchCompaniesSchema.use_providers.description,
113
+ enrichCompanySchema.use_providers.description,
114
+ enrichPersonSchema.use_providers.description,
95
115
  ].join('\n').toLowerCase()
96
116
 
97
117
  expect(publicMetadata).not.toContain('people data labs')
98
118
  expect(publicMetadata).not.toMatch(/\bpdl\b/)
99
119
  expect(publicMetadata).not.toContain('companyenrich')
100
120
  expect(publicMetadata).not.toContain('/companyenrich')
121
+ expect(publicMetadata).not.toMatch(/\bblitz(api)?\b/)
101
122
  })
102
123
  })
@@ -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,93 @@
1
+ import { readdirSync, readFileSync } from 'node:fs'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { z } from 'zod'
4
+
5
+ import { toOpenApiInputSchema } from '../src/tool-json-schema.js'
6
+ import { MCP_TOOLS } from './gtm/tools-bridge.js'
7
+
8
+ const PublicToolSchema = z.record(z.string(), z.instanceof(z.ZodType))
9
+
10
+ describe('public MCP tool JSON schemas', () => {
11
+ it('converts every registered public tool schema', async () => {
12
+ const toolFiles = readdirSync(new URL('../src/tools/', import.meta.url))
13
+ .filter((file) => file.endsWith('.ts'))
14
+ .sort()
15
+ const indexSource = readFileSync(new URL('../src/index.ts', import.meta.url), 'utf8')
16
+
17
+ expect(indexSource.match(/server\.tool\(/g)).toHaveLength(toolFiles.length)
18
+
19
+ for (const file of toolFiles) {
20
+ const exports = await import(new URL(`../src/tools/${file}`, import.meta.url).href)
21
+ const schemaEntries = Object.entries(exports).filter(([name]) => name.endsWith('Schema'))
22
+ const nameEntries = Object.entries(exports).filter(([name]) => name.endsWith('Name'))
23
+
24
+ expect(schemaEntries, file).toHaveLength(1)
25
+ expect(nameEntries, file).toHaveLength(1)
26
+
27
+ const schema = toOpenApiInputSchema(z.object(PublicToolSchema.parse(schemaEntries[0][1])))
28
+ expect(schema, z.string().parse(nameEntries[0][1])).toMatchObject({
29
+ type: 'object',
30
+ additionalProperties: false,
31
+ properties: expect.any(Object),
32
+ })
33
+ }
34
+ })
35
+
36
+ it('converts every tool schema without widening object inputs', () => {
37
+ for (const tool of MCP_TOOLS) {
38
+ const schema = toOpenApiInputSchema(tool.schema)
39
+
40
+ expect(schema, tool.name).toMatchObject({
41
+ type: 'object',
42
+ additionalProperties: false,
43
+ properties: expect.any(Object),
44
+ })
45
+ expect(schema, tool.name).not.toHaveProperty('$schema')
46
+ }
47
+ })
48
+
49
+ it('closes only Zod objects and does not change record or default data', () => {
50
+ const schema = toOpenApiInputSchema(z.object({
51
+ nested: z.object({ value: z.string() }),
52
+ labels: z.record(z.string(), z.string()),
53
+ settings: z.object({ type: z.literal('object') }).default({ type: 'object' }),
54
+ }))
55
+
56
+ expect(schema).toMatchObject({
57
+ properties: {
58
+ nested: { additionalProperties: false },
59
+ labels: { additionalProperties: { type: 'string' } },
60
+ settings: {
61
+ additionalProperties: false,
62
+ default: { type: 'object' },
63
+ },
64
+ },
65
+ })
66
+ })
67
+
68
+ it('preserves required fields, defaults, limits, and optional defaults', () => {
69
+ const tool = MCP_TOOLS.find((candidate) => candidate.name === 'find_influencers')
70
+ expect(tool).toBeDefined()
71
+ if (!tool) throw new Error('find_influencers is not registered')
72
+
73
+ const schema = toOpenApiInputSchema(tool.schema)
74
+
75
+ expect(schema.required).toEqual(['platform'])
76
+ expect(schema).toMatchObject({
77
+ properties: {
78
+ limit: {
79
+ type: 'integer',
80
+ minimum: 1,
81
+ maximum: 100,
82
+ default: 25,
83
+ },
84
+ page: {
85
+ type: 'integer',
86
+ minimum: 1,
87
+ default: 1,
88
+ },
89
+ },
90
+ })
91
+ expect(schema.properties?.page).not.toHaveProperty('nullable')
92
+ })
93
+ })
@@ -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')