@coldiq/mcp 0.3.4 → 0.3.6

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.
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { listDataSourcesHandler } from '../../src/tools/list-data-sources.js'
3
+
4
+ interface Source { name: string; strength?: string }
5
+ interface CapabilityCatalog { tool: string; label: string; sources: Source[] }
6
+
7
+ function parse(result: { content: { text: string }[]; isError?: boolean }) {
8
+ return JSON.parse(result.content[0].text)
9
+ }
10
+
11
+ function rowFor(caps: CapabilityCatalog[], toolContains: string): CapabilityCatalog | undefined {
12
+ return caps.find((c) => c.tool.includes(toolContains))
13
+ }
14
+
15
+ describe('list_data_sources handler', () => {
16
+ it('lists every capability-routed category with branded sources', async () => {
17
+ const parsed = parse(await listDataSourcesHandler({}))
18
+ const caps: CapabilityCatalog[] = parsed.data.capabilities
19
+ expect(parsed.data.intro).toContain('routes')
20
+ expect(caps.length).toBeGreaterThan(10)
21
+
22
+ const companies = rowFor(caps, 'search_companies')!
23
+ const names = companies.sources.map((s) => s.name)
24
+ expect(names).toContain('Apollo')
25
+ expect(names).toContain('TheirStack')
26
+
27
+ const emails = rowFor(caps, 'find_email')!
28
+ expect(emails.sources.map((s) => s.name)).toContain('FullEnrich')
29
+ })
30
+
31
+ it('never emits a raw slug or "Apify" in any source name', async () => {
32
+ const parsed = parse(await listDataSourcesHandler({}))
33
+ const caps: CapabilityCatalog[] = parsed.data.capabilities
34
+ for (const cap of caps) {
35
+ for (const src of cap.sources) {
36
+ expect(src.name).not.toContain('_')
37
+ expect(src.name.toLowerCase()).not.toContain('apify')
38
+ }
39
+ }
40
+ })
41
+
42
+ it('dedupes vendors within a category (one row per brand)', async () => {
43
+ const parsed = parse(await listDataSourcesHandler({}))
44
+ const caps: CapabilityCatalog[] = parsed.data.capabilities
45
+ const companies = rowFor(caps, 'search_companies')!
46
+ const names = companies.sources.map((s) => s.name)
47
+ // search_companies has limadata + limadata-prospect-* + linkupapi-* internally
48
+ expect(names.filter((n) => n === 'LimaData')).toHaveLength(1)
49
+ expect(names.filter((n) => n === 'LinkupAPI')).toHaveLength(1)
50
+ // every row carries the metadata a frontend needs
51
+ for (const cap of caps) {
52
+ expect(typeof cap.tool).toBe('string')
53
+ expect(typeof cap.label).toBe('string')
54
+ expect(cap.sources.length).toBeGreaterThan(0)
55
+ }
56
+ })
57
+
58
+ it('carries curated strengths on the well-known sources', async () => {
59
+ const parsed = parse(await listDataSourcesHandler({}))
60
+ const caps: CapabilityCatalog[] = parsed.data.capabilities
61
+ const theirstack = rowFor(caps, 'search_companies')!.sources.find((s) => s.name === 'TheirStack')!
62
+ expect(theirstack.strength).toBeTruthy()
63
+ expect(theirstack.strength).toContain('technographic')
64
+ })
65
+
66
+ it('filters to a single category by tool name', async () => {
67
+ const parsed = parse(await listDataSourcesHandler({ capability: 'find_email' }))
68
+ const caps: CapabilityCatalog[] = parsed.data.capabilities
69
+ expect(caps).toHaveLength(1)
70
+ expect(caps[0].tool).toContain('find_email')
71
+ })
72
+
73
+ it('accepts the find_emails alias for the shared email row', async () => {
74
+ const parsed = parse(await listDataSourcesHandler({ capability: 'find_emails' }))
75
+ expect(parsed.data.capabilities).toHaveLength(1)
76
+ expect(parsed.data.capabilities[0].tool).toContain('find_email')
77
+ })
78
+
79
+ it('returns an error payload listing valid values for an unknown category', async () => {
80
+ const result = await listDataSourcesHandler({ capability: 'nonsense' })
81
+ expect(result.isError).toBe(true)
82
+ const parsed = parse(result)
83
+ expect(parsed.error).toContain('not a recognized category')
84
+ expect(parsed.valid_capabilities).toContain('search_companies')
85
+ })
86
+ })
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { providerDisplayName } from '../../src/utils/provider-display.js'
3
+
4
+ describe('providerDisplayName', () => {
5
+ it('maps known vendor ids to branded names', () => {
6
+ expect(providerDisplayName('fullenrich')).toBe('FullEnrich')
7
+ expect(providerDisplayName('theirstack')).toBe('TheirStack')
8
+ expect(providerDisplayName('pdl')).toBe('People Data Labs')
9
+ })
10
+
11
+ it('collapses vendor variants to one brand', () => {
12
+ expect(providerDisplayName('limadata-work-email')).toBe('LimaData')
13
+ expect(providerDisplayName('limadata-prospect-url')).toBe('LimaData')
14
+ expect(providerDisplayName('signalbase-funding')).toBe('SignalBase')
15
+ expect(providerDisplayName('linkupapi-by-domain')).toBe('LinkupAPI')
16
+ })
17
+
18
+ it('brands Apify-backed scrapers as their data source, never raw slug', () => {
19
+ expect(providerDisplayName('reddit_ads')).toBe('Reddit Ads')
20
+ expect(providerDisplayName('linkedin_ad_library')).toBe('LinkedIn Ad Library')
21
+ expect(providerDisplayName('google_maps')).toBe('Google Maps')
22
+ })
23
+
24
+ it('title-cases unmapped ids as a defensive fallback', () => {
25
+ expect(providerDisplayName('some-new-vendor')).toBe('Some New Vendor')
26
+ expect(providerDisplayName('good')).toBe('Good')
27
+ })
28
+
29
+ it('never returns a raw slug or the word "Apify"', () => {
30
+ const ids = [
31
+ 'limadata-work-email', 'reddit_ads', 'fullenrich', 'ai-ark-people',
32
+ 'linkupapi-validate', 'theirstack-buying-intents', 'completely_unknown_slug',
33
+ ]
34
+ for (const id of ids) {
35
+ const name = providerDisplayName(id)
36
+ expect(name).not.toContain('_')
37
+ expect(name.toLowerCase()).not.toContain('apify')
38
+ }
39
+ })
40
+ })
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { buildSelectionInsight } from '../../src/utils/selection-insight.js'
3
+
4
+ describe('buildSelectionInsight', () => {
5
+ describe('capability routing (gated-in, not fallback)', () => {
6
+ it('ties the choice to the distinctive input signal', () => {
7
+ const r = buildSelectionInsight(
8
+ 'search_companies',
9
+ 'theirstack',
10
+ { technologies: ['Salesforce'], keywords: ['saas'] },
11
+ { wasFallback: false, pinnedByUser: false },
12
+ )
13
+ expect(r).toBeDefined()
14
+ expect(r!.insight).toContain('TheirStack')
15
+ expect(r!.insight).toContain('technographic')
16
+ // tech-stack is the most distinctive signal → drives the "matched to your …" clause
17
+ expect(r!.insight).toContain('tech-stack filter')
18
+ expect(r!.insight.startsWith('Routed to')).toBe(true)
19
+ expect(r!.signals[0]).toBe('tech-stack filter')
20
+ })
21
+
22
+ it('omits the signal clause when no distinctive input is present', () => {
23
+ const r = buildSelectionInsight(
24
+ 'enrich_company',
25
+ 'apollo',
26
+ {},
27
+ { wasFallback: false, pinnedByUser: false },
28
+ )
29
+ expect(r!.insight).toBe('Routed to Apollo for its deep firmographic enrichment.')
30
+ expect(r!.signals).toEqual([])
31
+ })
32
+ })
33
+
34
+ describe('fallthrough (softened wording)', () => {
35
+ it('states coverage without claiming it was the upfront best fit', () => {
36
+ const r = buildSelectionInsight(
37
+ 'find_email',
38
+ 'fullenrich',
39
+ { domain: 'coldiq.com', first_name: 'Michel' },
40
+ { wasFallback: true, pinnedByUser: false },
41
+ )
42
+ expect(r!.insight).toContain('FullEnrich')
43
+ expect(r!.insight).toContain('returned the match here')
44
+ // never the confident "Routed to" framing on a fallthrough
45
+ expect(r!.insight.startsWith('Routed to')).toBe(false)
46
+ })
47
+ })
48
+
49
+ describe('user-pinned providers', () => {
50
+ it('does not claim ColdIQ chose it', () => {
51
+ const r = buildSelectionInsight(
52
+ 'search_companies',
53
+ 'theirstack',
54
+ { technologies: ['Salesforce'] },
55
+ { wasFallback: false, pinnedByUser: true },
56
+ )
57
+ expect(r!.insight).toBe('Used TheirStack as requested.')
58
+ // signals still surfaced for UI
59
+ expect(r!.signals).toContain('tech-stack filter')
60
+ })
61
+ })
62
+
63
+ describe('no curated edge — plain true line, no fabrication', () => {
64
+ it('emits a generic line for an unmapped provider', () => {
65
+ const r = buildSelectionInsight(
66
+ 'enrich_company',
67
+ 'good', // synthetic id, not in STRENGTHS
68
+ { domain: 'coldiq.com' },
69
+ { wasFallback: false, pinnedByUser: false },
70
+ )
71
+ expect(r!.insight).toBe('Good matched your company domain for this company enrichment.')
72
+ // no invented "deep/strong/broad coverage" claim
73
+ expect(r!.insight).not.toMatch(/deep|strong|broad|coverage|specializ/)
74
+ })
75
+
76
+ it('falls back to the barest true line with no signals', () => {
77
+ const r = buildSelectionInsight(
78
+ 'search_reddit',
79
+ 'mystery_provider',
80
+ {},
81
+ { wasFallback: false, pinnedByUser: false },
82
+ )
83
+ expect(r!.insight).toBe('Mystery Provider matched this Reddit search.')
84
+ })
85
+ })
86
+
87
+ describe('find_emails batch signals', () => {
88
+ it('detects LinkedIn URLs and name+domain across people', () => {
89
+ const r = buildSelectionInsight(
90
+ 'find_emails',
91
+ 'fullenrich',
92
+ { people: [{ id: '1', first_name: 'Michel', domain: 'coldiq.com', linkedin_url: 'https://linkedin.com/in/x' }] },
93
+ { wasFallback: true, pinnedByUser: false },
94
+ )
95
+ expect(r!.insight).toContain('FullEnrich')
96
+ expect(r!.signals).toContain('LinkedIn URLs')
97
+ expect(r!.signals).toContain('name + domain')
98
+ })
99
+ })
100
+
101
+ describe('honesty — fallthrough never claims an upfront best-fit judgment', () => {
102
+ it('uses the softened framing and no superiority superlatives', () => {
103
+ const r = buildSelectionInsight(
104
+ 'find_people',
105
+ 'apollo',
106
+ { job_titles: ['CEO'] },
107
+ { wasFallback: true, pinnedByUser: false },
108
+ )
109
+ expect(r!.insight).toContain('returned the match here')
110
+ expect(r!.insight.startsWith('Routed to')).toBe(false)
111
+ expect(r!.insight.toLowerCase()).not.toMatch(/\bbest\b|deepest|largest/)
112
+ })
113
+ })
114
+ })