@coldiq/mcp 0.3.9 → 0.3.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.
@@ -13,7 +13,7 @@ describe('search_companies handler', () => {
13
13
  globalThis.fetch = originalFetch
14
14
  })
15
15
 
16
- it('returns results from first successful provider', async () => {
16
+ it('returns results from first successful provider (compact by default)', async () => {
17
17
  globalThis.fetch = vi.fn(async () =>
18
18
  new Response(JSON.stringify({ data: [{ name: 'ColdIQ', domain: 'coldiq.com' }] }), { status: 200 })
19
19
  ) as typeof fetch
@@ -23,7 +23,83 @@ describe('search_companies handler', () => {
23
23
  expect(result.isError).toBeUndefined()
24
24
  const parsed = JSON.parse(result.content[0].text)
25
25
  expect(parsed._meta.provider).toBe('companyenrich')
26
- expect(parsed.data.data).toHaveLength(1)
26
+ // Compact-by-default: the raw provider array is normalized into companies[].
27
+ expect(parsed.data.companies).toHaveLength(1)
28
+ expect(parsed.data.companies[0]).toMatchObject({ name: 'ColdIQ', domain: 'coldiq.com' })
29
+ })
30
+
31
+ it('compact (default) keeps identity + firmographic fields and drops heavy ones', async () => {
32
+ // Mirror the real CompanyEnrich shape: companies under data.items[], each a
33
+ // heavy object with funding/technologies/keywords/socials/seo.
34
+ globalThis.fetch = vi.fn(async () =>
35
+ new Response(JSON.stringify({
36
+ items: [{
37
+ id: '0192562e-7008-702b-97a2-0b2c489d339b',
38
+ name: 'Hugging Face',
39
+ domain: 'huggingface.co',
40
+ website: 'https://huggingface.co',
41
+ industry: 'Business Services',
42
+ categories: ['b2b', 'b2c', 'saas'],
43
+ employees: '201-500',
44
+ revenue: '10m-50m',
45
+ founded_year: 2016,
46
+ description: 'A very long company description that should be dropped in compact mode...',
47
+ keywords: Array.from({ length: 28 }, (_, i) => `kw${i}`),
48
+ technologies: ['Stripe', 'Nginx', 'Node-Js'],
49
+ naics_codes: ['511210', '518210'],
50
+ socials: { linkedin_url: 'https://www.linkedin.com/company/huggingface', twitter_url: 'https://twitter.com/huggingface' },
51
+ financial: { total_funding: 395200000, funding: [{ date: '2024-08-01', type: 'Venture Round' }] },
52
+ location: { country: { code: 'FR', name: 'France' }, city: { name: 'Paris' } },
53
+ }],
54
+ page: 1,
55
+ totalItems: 10000,
56
+ }), { status: 200 })
57
+ ) as typeof fetch
58
+
59
+ const result = await searchCompaniesHandler({ keywords: ['SaaS'], countries: ['FR'], limit: 5 })
60
+
61
+ expect(result.isError).toBeUndefined()
62
+ const parsed = JSON.parse(result.content[0].text)
63
+ expect(parsed.data.total).toBe(10000)
64
+ expect(parsed.data.companies).toHaveLength(1)
65
+ const c = parsed.data.companies[0]
66
+ // Kept: identity + firmographics + downstream-critical linkedin_url.
67
+ expect(c).toEqual({
68
+ name: 'Hugging Face',
69
+ domain: 'huggingface.co',
70
+ website: 'https://huggingface.co',
71
+ linkedin_url: 'https://www.linkedin.com/company/huggingface',
72
+ employees: '201-500',
73
+ revenue: '10m-50m',
74
+ industry: 'Business Services',
75
+ country: 'France',
76
+ city: 'Paris',
77
+ founded_year: 2016,
78
+ categories: ['b2b', 'b2c', 'saas'],
79
+ })
80
+ // Dropped heavy fields.
81
+ expect(c.description).toBeUndefined()
82
+ expect(c.keywords).toBeUndefined()
83
+ expect(c.technologies).toBeUndefined()
84
+ expect(c.naics_codes).toBeUndefined()
85
+ expect(c.financial).toBeUndefined()
86
+ expect(c.id).toBeUndefined()
87
+ })
88
+
89
+ it('fields=verbose returns the raw provider passthrough unchanged', async () => {
90
+ globalThis.fetch = vi.fn(async () =>
91
+ new Response(JSON.stringify({ items: [{ name: 'Hugging Face', keywords: ['ai'], technologies: ['Stripe'] }], totalItems: 10000 }), { status: 200 })
92
+ ) as typeof fetch
93
+
94
+ const result = await searchCompaniesHandler({ keywords: ['SaaS'], limit: 5, fields: 'verbose' })
95
+
96
+ expect(result.isError).toBeUndefined()
97
+ const parsed = JSON.parse(result.content[0].text)
98
+ // Raw shape preserved: items[] with heavy fields, no companies[] normalization.
99
+ expect(parsed.data.items).toHaveLength(1)
100
+ expect(parsed.data.items[0].keywords).toEqual(['ai'])
101
+ expect(parsed.data.items[0].technologies).toEqual(['Stripe'])
102
+ expect(parsed.data.companies).toBeUndefined()
27
103
  })
28
104
 
29
105
  it('falls back to FullEnrich (priority 2) on CompanyEnrich failure', async () => {
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ compactCompaniesPayload,
4
+ extractCompaniesArray,
5
+ normalizeCompany,
6
+ } from '../../src/utils/compact-companies.js'
7
+
8
+ describe('extractCompaniesArray', () => {
9
+ it('finds the array under each provider key', () => {
10
+ expect(extractCompaniesArray({ items: [{ name: 'A' }] })).toHaveLength(1) // CompanyEnrich
11
+ expect(extractCompaniesArray({ companies: [{ name: 'A' }] })).toHaveLength(1) // FullEnrich
12
+ expect(extractCompaniesArray({ organizations: [{ name: 'A' }, { name: 'B' }] })).toHaveLength(2) // Apollo
13
+ expect(extractCompaniesArray({ data: [{ name: 'A' }] })).toHaveLength(1) // generic
14
+ expect(extractCompaniesArray({ results: [{ name: 'A' }] })).toHaveLength(1)
15
+ })
16
+
17
+ it('handles a bare top-level array', () => {
18
+ expect(extractCompaniesArray([{ name: 'A' }])).toHaveLength(1)
19
+ })
20
+
21
+ it('finds AI-Ark content[] and LinkUp nested data.companies[]', () => {
22
+ // AI-Ark companies live under content[] (registry hasResult: isNonEmptyArray(d.content)).
23
+ expect(extractCompaniesArray({ content: [{ name: 'A' }, { name: 'B' }] })).toHaveLength(2)
24
+ // LinkUp (search/fundraising/hiring) nests under data.companies[]
25
+ // (registry hasResult: isNonEmptyArray(d.data.companies)).
26
+ expect(extractCompaniesArray({ data: { companies: [{ name: 'A' }] } })).toHaveLength(1)
27
+ expect(extractCompaniesArray({ data: { organizations: [{ name: 'A' }, { name: 'B' }] } })).toHaveLength(2)
28
+ })
29
+
30
+ it('prefers a direct array under data over descending into a nested object', () => {
31
+ // PDL/Prospeo/TheirStack shape: { data: [...] } must stay flat, not be skipped.
32
+ expect(extractCompaniesArray({ data: [{ name: 'A' }] })).toHaveLength(1)
33
+ })
34
+
35
+ it('returns [] for null / non-object / no recognized key', () => {
36
+ expect(extractCompaniesArray(null)).toEqual([])
37
+ expect(extractCompaniesArray('nope')).toEqual([])
38
+ expect(extractCompaniesArray({ totalItems: 10 })).toEqual([])
39
+ })
40
+ })
41
+
42
+ describe('normalizeCompany', () => {
43
+ it('maps the CompanyEnrich nested shape to compact fields', () => {
44
+ const c = normalizeCompany({
45
+ id: 'uuid-123',
46
+ name: 'Mistral AI',
47
+ domain: 'mistral.ai',
48
+ website: 'https://mistral.ai',
49
+ industry: 'Software',
50
+ categories: ['b2b', 'saas'],
51
+ employees: '201-500',
52
+ revenue: '1m-10m',
53
+ founded_year: 2023,
54
+ description: 'long blurb',
55
+ keywords: ['a', 'b'],
56
+ technologies: ['Gmail'],
57
+ socials: { linkedin_url: 'https://www.linkedin.com/company/mistralai' },
58
+ location: { country: { code: 'FR', name: 'France' }, city: { name: 'Paris' } },
59
+ })
60
+ expect(c).toEqual({
61
+ name: 'Mistral AI',
62
+ domain: 'mistral.ai',
63
+ website: 'https://mistral.ai',
64
+ linkedin_url: 'https://www.linkedin.com/company/mistralai',
65
+ employees: '201-500',
66
+ revenue: '1m-10m',
67
+ industry: 'Software',
68
+ country: 'France',
69
+ city: 'Paris',
70
+ founded_year: 2023,
71
+ categories: ['b2b', 'saas'],
72
+ })
73
+ })
74
+
75
+ it('maps the Apollo organization shape (numeric employees, flat fields)', () => {
76
+ const c = normalizeCompany({
77
+ name: 'Apple',
78
+ primary_domain: 'apple.com',
79
+ website_url: 'https://apple.com',
80
+ linkedin_url: 'https://www.linkedin.com/company/apple',
81
+ estimated_num_employees: 164000,
82
+ industry: 'consumer electronics',
83
+ founded_year: 1976,
84
+ })
85
+ expect(c).toMatchObject({
86
+ name: 'Apple',
87
+ domain: 'apple.com',
88
+ website: 'https://apple.com',
89
+ linkedin_url: 'https://www.linkedin.com/company/apple',
90
+ employees: 164000,
91
+ industry: 'consumer electronics',
92
+ founded_year: 1976,
93
+ })
94
+ })
95
+
96
+ it('maps BlitzAPI employees_on_linkedin into employees', () => {
97
+ const c = normalizeCompany({ name: 'Blitz Co', domain: 'blitz.co', employees_on_linkedin: 320 })
98
+ expect(c).toMatchObject({ name: 'Blitz Co', domain: 'blitz.co', employees: 320 })
99
+ })
100
+
101
+ it('rejects a non-LinkedIn url in the linkedin slot', () => {
102
+ const c = normalizeCompany({ name: 'X', socials: { linkedin_url: 'https://twitter.com/x' } })
103
+ expect(c?.linkedin_url).toBeUndefined()
104
+ expect(c?.name).toBe('X')
105
+ })
106
+
107
+ it('returns null for empty / non-object records', () => {
108
+ expect(normalizeCompany(null)).toBeNull()
109
+ expect(normalizeCompany({})).toBeNull()
110
+ expect(normalizeCompany([{ name: 'A' }])).toBeNull()
111
+ expect(normalizeCompany('x')).toBeNull()
112
+ })
113
+
114
+ it('omits fields that are not present', () => {
115
+ const c = normalizeCompany({ name: 'Solo' })
116
+ expect(c).toEqual({ name: 'Solo' })
117
+ })
118
+ })
119
+
120
+ describe('compactCompaniesPayload', () => {
121
+ it('normalizes items[] and surfaces totalItems', () => {
122
+ const out = compactCompaniesPayload({
123
+ items: [{ name: 'A', domain: 'a.com' }, { name: 'B', domain: 'b.com' }],
124
+ totalItems: 10000,
125
+ })
126
+ expect(out.companies).toHaveLength(2)
127
+ expect(out.total).toBe(10000)
128
+ })
129
+
130
+ it('reads Apollo pagination.total_entries as total', () => {
131
+ const out = compactCompaniesPayload({ organizations: [{ name: 'A' }], pagination: { total_entries: 8371 } })
132
+ expect(out.companies).toHaveLength(1)
133
+ expect(out.total).toBe(8371)
134
+ })
135
+
136
+ it('drops records that normalize to nothing but keeps real ones', () => {
137
+ const out = compactCompaniesPayload({ items: [{ name: 'A' }, {}, { foo: 'bar' }] })
138
+ expect(out.companies).toHaveLength(1)
139
+ expect(out.companies[0].name).toBe('A')
140
+ })
141
+
142
+ it('returns an empty companies array (no total) when there is nothing to extract', () => {
143
+ const out = compactCompaniesPayload({ unexpected: true })
144
+ expect(out).toEqual({ companies: [] })
145
+ })
146
+ })