@coldiq/mcp 0.1.11 → 0.1.13
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/package.json +5 -1
- package/src/client.ts +31 -1
- package/src/executor.ts +36 -14
- package/src/registry.ts +111 -29
- package/src/tools/enrich-company.ts +4 -4
- package/src/tools/enrich-person.ts +3 -3
- package/src/tools/fetch-page-content.ts +3 -3
- package/src/tools/find-email.ts +4 -4
- package/src/tools/find-emails.ts +69 -5
- package/src/tools/find-influencers.ts +3 -3
- package/src/tools/find-people.ts +5 -5
- package/src/tools/find-phone.ts +4 -4
- package/src/tools/find-signals.ts +5 -5
- package/src/tools/search-ads.ts +3 -3
- package/src/tools/search-companies.ts +3 -3
- package/src/tools/search-jobs.ts +4 -4
- package/src/tools/search-places.ts +3 -3
- package/src/tools/search-reddit.ts +3 -3
- package/src/tools/search-seo.ts +3 -3
- package/src/tools/search-web.ts +3 -3
- package/src/tools/verify-email.ts +3 -3
- package/tests/client.test.ts +32 -0
- package/tests/executor.test.ts +81 -0
- package/tests/live/companyenrich-async-probe.ts +31 -0
- package/tests/live/companyenrich-fix-validation.ts +125 -0
- package/tests/live/companyenrich-hybrid-probe.ts +54 -0
- package/tests/live/companyenrich-query-probe.ts +49 -0
- package/tests/live/companyenrich-ranges-probe.ts +49 -0
- package/tests/live/companyenrich-upstream-probe.ts +72 -0
- package/tests/live/wework-brazil-trace.ts +200 -0
- package/tests/registry-find-people.test.ts +111 -0
- package/tests/registry-polling.test.ts +61 -0
- package/tests/registry.test.ts +35 -18
- package/tests/tools/find-emails.test.ts +108 -0
- package/tests/tools/response-format.test.ts +45 -0
package/src/tools/find-phone.ts
CHANGED
|
@@ -42,19 +42,19 @@ export async function findPhoneHandler(input: Record<string, unknown>) {
|
|
|
42
42
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
43
43
|
const resolved = resolvePreferredProviders('find_phone', restInput, rawUseProviders)
|
|
44
44
|
if (!resolved.ok) {
|
|
45
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
45
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
46
46
|
}
|
|
47
47
|
const validation = inputSchema.safeParse(restInput)
|
|
48
48
|
if (!validation.success) {
|
|
49
49
|
const msg = validation.error.issues.map((i) => i.message).join('; ')
|
|
50
50
|
return {
|
|
51
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }
|
|
51
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }) }],
|
|
52
52
|
isError: true,
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
const result = await executeWithFallback('find_phone', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
56
56
|
if (isExecutionError(result)) {
|
|
57
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
57
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
58
58
|
}
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
59
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
60
60
|
}
|
|
@@ -60,7 +60,7 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
|
|
|
60
60
|
return {
|
|
61
61
|
content: [{
|
|
62
62
|
type: 'text' as const,
|
|
63
|
-
text: JSON.stringify({ error: 'intent signal_type requires at least one of: companies or domains' }
|
|
63
|
+
text: JSON.stringify({ error: 'intent signal_type requires at least one of: companies or domains' }),
|
|
64
64
|
}],
|
|
65
65
|
isError: true,
|
|
66
66
|
}
|
|
@@ -72,7 +72,7 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
|
|
|
72
72
|
type: 'text' as const,
|
|
73
73
|
text: JSON.stringify({
|
|
74
74
|
error: 'news and startup_post are feed-style signal types that only filter by country and since date — they do not support company filtering. Remove companies/domains, or use signal_type=intent for company-targeted intent signals.',
|
|
75
|
-
}
|
|
75
|
+
}),
|
|
76
76
|
}],
|
|
77
77
|
isError: true,
|
|
78
78
|
}
|
|
@@ -80,12 +80,12 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
|
|
|
80
80
|
|
|
81
81
|
const resolved = resolvePreferredProviders('find_signals', restInput, rawUseProviders)
|
|
82
82
|
if (!resolved.ok) {
|
|
83
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
83
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const result = await executeWithFallback('find_signals', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
87
87
|
if (isExecutionError(result)) {
|
|
88
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
88
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// buying_intents upstream has no limit param — truncate to requested limit
|
|
@@ -97,5 +97,5 @@ export async function findSignalsHandler(input: Record<string, unknown>) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
100
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
101
101
|
}
|
package/src/tools/search-ads.ts
CHANGED
|
@@ -41,11 +41,11 @@ export async function searchAdsHandler(input: Record<string, unknown>) {
|
|
|
41
41
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
42
42
|
const resolved = resolvePreferredProviders('search_ads', restInput, rawUseProviders)
|
|
43
43
|
if (!resolved.ok) {
|
|
44
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
44
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
45
45
|
}
|
|
46
46
|
const result = await executeWithFallback('search_ads', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
47
47
|
if (isExecutionError(result)) {
|
|
48
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
48
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
49
49
|
}
|
|
50
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
50
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
51
51
|
}
|
|
@@ -38,11 +38,11 @@ export async function searchCompaniesHandler(input: Record<string, unknown>) {
|
|
|
38
38
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
39
39
|
const resolved = resolvePreferredProviders('search_companies', restInput, rawUseProviders)
|
|
40
40
|
if (!resolved.ok) {
|
|
41
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
41
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
42
42
|
}
|
|
43
43
|
const result = await executeWithFallback('search_companies', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
44
44
|
if (isExecutionError(result)) {
|
|
45
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
45
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
46
46
|
}
|
|
47
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
47
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
48
48
|
}
|
package/src/tools/search-jobs.ts
CHANGED
|
@@ -66,15 +66,15 @@ export async function searchJobsHandler(input: Record<string, unknown>) {
|
|
|
66
66
|
'Contradictory filters: ATS/domain filters (ats_slugs, company_domains) are Career Site only, ' +
|
|
67
67
|
'while seniority/industry/employee filters are LinkedIn only. Remove one set.',
|
|
68
68
|
}
|
|
69
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(err
|
|
69
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(err) }], isError: true }
|
|
70
70
|
}
|
|
71
71
|
const resolved = resolvePreferredProviders('search_jobs', restInput, rawUseProviders)
|
|
72
72
|
if (!resolved.ok) {
|
|
73
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
73
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
74
74
|
}
|
|
75
75
|
const result = await executeWithFallback('search_jobs', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
76
76
|
if (isExecutionError(result)) {
|
|
77
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
77
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
78
78
|
}
|
|
79
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
79
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
80
80
|
}
|
|
@@ -49,11 +49,11 @@ export async function searchPlacesHandler(input: Record<string, unknown>) {
|
|
|
49
49
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
50
50
|
const resolved = resolvePreferredProviders('search_places', restInput, rawUseProviders)
|
|
51
51
|
if (!resolved.ok) {
|
|
52
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
52
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
53
53
|
}
|
|
54
54
|
const result = await executeWithFallback('search_places', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
55
55
|
if (isExecutionError(result)) {
|
|
56
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
56
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
57
57
|
}
|
|
58
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
58
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
59
59
|
}
|
|
@@ -31,11 +31,11 @@ export async function searchRedditHandler(input: Record<string, unknown>) {
|
|
|
31
31
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
32
32
|
const resolved = resolvePreferredProviders('search_reddit', restInput, rawUseProviders)
|
|
33
33
|
if (!resolved.ok) {
|
|
34
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
34
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
35
35
|
}
|
|
36
36
|
const result = await executeWithFallback('search_reddit', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
37
37
|
if (isExecutionError(result)) {
|
|
38
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
38
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
39
39
|
}
|
|
40
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
40
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
41
41
|
}
|
package/src/tools/search-seo.ts
CHANGED
|
@@ -56,11 +56,11 @@ export async function searchSeoHandler(input: Record<string, unknown>) {
|
|
|
56
56
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
57
57
|
const resolved = resolvePreferredProviders('search_seo', restInput, rawUseProviders)
|
|
58
58
|
if (!resolved.ok) {
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
59
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
60
60
|
}
|
|
61
61
|
const result = await executeWithFallback('search_seo', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
62
62
|
if (isExecutionError(result)) {
|
|
63
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
63
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
64
64
|
}
|
|
65
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
65
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
66
66
|
}
|
package/src/tools/search-web.ts
CHANGED
|
@@ -22,11 +22,11 @@ export async function searchWebHandler(input: Record<string, unknown>) {
|
|
|
22
22
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
23
23
|
const resolved = resolvePreferredProviders('search_web', restInput, rawUseProviders)
|
|
24
24
|
if (!resolved.ok) {
|
|
25
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
25
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
26
26
|
}
|
|
27
27
|
const result = await executeWithFallback('search_web', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
28
28
|
if (isExecutionError(result)) {
|
|
29
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
29
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
30
30
|
}
|
|
31
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
31
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
32
32
|
}
|
|
@@ -16,11 +16,11 @@ export async function verifyEmailHandler(input: Record<string, unknown>) {
|
|
|
16
16
|
const { use_providers: rawUseProviders, ...restInput } = input
|
|
17
17
|
const resolved = resolvePreferredProviders('verify_email', restInput, rawUseProviders)
|
|
18
18
|
if (!resolved.ok) {
|
|
19
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error
|
|
19
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
20
20
|
}
|
|
21
21
|
const result = await executeWithFallback('verify_email', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
22
22
|
if (isExecutionError(result)) {
|
|
23
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
23
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
24
24
|
}
|
|
25
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result
|
|
25
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
26
26
|
}
|
package/tests/client.test.ts
CHANGED
|
@@ -87,4 +87,36 @@ describe('client', () => {
|
|
|
87
87
|
expect(result.ok).toBe(false)
|
|
88
88
|
expect(result.status).toBe(404)
|
|
89
89
|
})
|
|
90
|
+
|
|
91
|
+
it('maps AbortError (timeout) to { error: "Request timed out" }', async () => {
|
|
92
|
+
globalThis.fetch = vi.fn(async (_url: unknown, init?: RequestInit) => {
|
|
93
|
+
const signal = init?.signal as AbortSignal | undefined
|
|
94
|
+
if (signal?.aborted) {
|
|
95
|
+
const err = new Error('aborted')
|
|
96
|
+
err.name = 'AbortError'
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
await new Promise<void>((_, reject) => {
|
|
100
|
+
signal?.addEventListener('abort', () => {
|
|
101
|
+
const err = new Error('aborted')
|
|
102
|
+
err.name = 'AbortError'
|
|
103
|
+
reject(err)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
return new Response(JSON.stringify({}), { status: 200 })
|
|
107
|
+
}) as typeof fetch
|
|
108
|
+
|
|
109
|
+
const origTimeout = process.env.COLDIQ_HTTP_TIMEOUT_MS
|
|
110
|
+
process.env.COLDIQ_HTTP_TIMEOUT_MS = '1'
|
|
111
|
+
initClient('http://test-api.local', 'test-key-123')
|
|
112
|
+
try {
|
|
113
|
+
const res = await callApi('GET', '/test')
|
|
114
|
+
expect(res.ok).toBe(false)
|
|
115
|
+
expect(res.status).toBe(0)
|
|
116
|
+
expect((res.data as Record<string, unknown>).error).toBe('Request timed out')
|
|
117
|
+
} finally {
|
|
118
|
+
if (origTimeout === undefined) delete process.env.COLDIQ_HTTP_TIMEOUT_MS
|
|
119
|
+
else process.env.COLDIQ_HTTP_TIMEOUT_MS = origTimeout
|
|
120
|
+
}
|
|
121
|
+
})
|
|
90
122
|
})
|
package/tests/executor.test.ts
CHANGED
|
@@ -541,6 +541,87 @@ describe('executor async polling intervals', () => {
|
|
|
541
541
|
expect(requestedDelays).toEqual([100, 200, 300, 400])
|
|
542
542
|
})
|
|
543
543
|
|
|
544
|
+
it('first poll fires after firstPollMs (≤ configured interval) — not after the full interval', async () => {
|
|
545
|
+
const delays: number[] = []
|
|
546
|
+
globalThis.setTimeout = ((cb: () => void, ms?: number) => {
|
|
547
|
+
if (typeof ms === 'number' && ms > 0) delays.push(ms)
|
|
548
|
+
cb()
|
|
549
|
+
return 0 as unknown as ReturnType<typeof setTimeout>
|
|
550
|
+
}) as typeof setTimeout
|
|
551
|
+
|
|
552
|
+
let pollCount = 0
|
|
553
|
+
stubProviders([
|
|
554
|
+
makeProvider({
|
|
555
|
+
id: 'fast-complete',
|
|
556
|
+
hasResult: (data) => (data as Record<string, unknown>).done === true,
|
|
557
|
+
async: {
|
|
558
|
+
extractId: () => 'job-1',
|
|
559
|
+
pollEndpoint: (id) => `/poll/${id}`,
|
|
560
|
+
pollIntervalMs: 5000, // long interval
|
|
561
|
+
timeoutMs: 60_000,
|
|
562
|
+
isComplete: (data) => (data as Record<string, unknown>).done === true,
|
|
563
|
+
},
|
|
564
|
+
}),
|
|
565
|
+
])
|
|
566
|
+
|
|
567
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
568
|
+
const u = url.toString()
|
|
569
|
+
if (u.includes('/poll/')) {
|
|
570
|
+
pollCount++
|
|
571
|
+
return new Response(JSON.stringify({ done: true }), { status: 200 })
|
|
572
|
+
}
|
|
573
|
+
return new Response(JSON.stringify({ id: 'job-1' }), { status: 200 })
|
|
574
|
+
}) as typeof fetch
|
|
575
|
+
|
|
576
|
+
await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
577
|
+
|
|
578
|
+
// First poll fired after ≤ 750ms (COLDIQ_FIRST_POLL_MS default), which is less
|
|
579
|
+
// than the configured 5000ms interval.
|
|
580
|
+
expect(pollCount).toBe(1)
|
|
581
|
+
expect(delays.length).toBe(1)
|
|
582
|
+
expect(delays[0]).toBeLessThanOrEqual(750)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('consecutive poll errors up to maxPollErrors abort the async job', async () => {
|
|
586
|
+
globalThis.setTimeout = ((cb: () => void) => { cb(); return 0 as unknown as ReturnType<typeof setTimeout> }) as typeof setTimeout
|
|
587
|
+
|
|
588
|
+
let createCalled = false
|
|
589
|
+
stubProviders([
|
|
590
|
+
makeProvider({
|
|
591
|
+
id: 'stuck-poll',
|
|
592
|
+
hasResult: () => false,
|
|
593
|
+
async: {
|
|
594
|
+
extractId: () => 'job-stuck',
|
|
595
|
+
pollEndpoint: (id) => `/poll/${id}`,
|
|
596
|
+
pollIntervalMs: 10,
|
|
597
|
+
timeoutMs: 60_000,
|
|
598
|
+
isComplete: () => false,
|
|
599
|
+
},
|
|
600
|
+
}),
|
|
601
|
+
])
|
|
602
|
+
|
|
603
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
604
|
+
const u = url.toString()
|
|
605
|
+
if (u.includes('/poll/')) {
|
|
606
|
+
return new Response(JSON.stringify({ error: 'Internal error' }), { status: 500 })
|
|
607
|
+
}
|
|
608
|
+
createCalled = true
|
|
609
|
+
return new Response(JSON.stringify({ id: 'job-stuck' }), { status: 200 })
|
|
610
|
+
}) as typeof fetch
|
|
611
|
+
|
|
612
|
+
const origMax = process.env.COLDIQ_MAX_POLL_ERRORS
|
|
613
|
+
process.env.COLDIQ_MAX_POLL_ERRORS = '3'
|
|
614
|
+
try {
|
|
615
|
+
const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
|
|
616
|
+
expect(createCalled).toBe(true)
|
|
617
|
+
expect('error' in result).toBe(true)
|
|
618
|
+
// Should return in well under the 60s timeout (no setTimeout mocking needed beyond immediate)
|
|
619
|
+
} finally {
|
|
620
|
+
if (origMax === undefined) delete process.env.COLDIQ_MAX_POLL_ERRORS
|
|
621
|
+
else process.env.COLDIQ_MAX_POLL_ERRORS = origMax
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
|
|
544
625
|
})
|
|
545
626
|
|
|
546
627
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Probe /companies/search/async body shape — does it want filters flat under `search`
|
|
2
|
+
// or still wrapped under `search.filters`?
|
|
3
|
+
const KEY = process.env.COMPANYENRICH_API_KEY!
|
|
4
|
+
const BASE = 'https://api.companyenrich.com'
|
|
5
|
+
|
|
6
|
+
async function call(label: string, body: unknown) {
|
|
7
|
+
const t0 = Date.now()
|
|
8
|
+
const res = await fetch(`${BASE}/companies/search/async`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
signal: AbortSignal.timeout(20_000),
|
|
13
|
+
})
|
|
14
|
+
let data: any
|
|
15
|
+
try { data = await res.json() } catch { data = '<non-json>' }
|
|
16
|
+
console.log(`[${label}]\n body=${JSON.stringify(body)}\n status=${res.status} ms=${Date.now() - t0} data=${JSON.stringify(data).slice(0, 300)}\n`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
// Use count=1 to minimise credit burn.
|
|
21
|
+
await call('A: search.filters wrapped (current)', {
|
|
22
|
+
search: { filters: { query: 'wework' } }, count: 1,
|
|
23
|
+
})
|
|
24
|
+
await call('B: search flat (proposed)', {
|
|
25
|
+
search: { query: 'wework' }, count: 1,
|
|
26
|
+
})
|
|
27
|
+
// Some async APIs accept query at the very top too.
|
|
28
|
+
await call('C: query at top, no search envelope', { query: 'wework', count: 1 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Validation suite — confirms the fix direction before committing.
|
|
2
|
+
//
|
|
3
|
+
// What we need to prove:
|
|
4
|
+
// 1. `{filters: {...}}` wrapping is broken across the FOUR search endpoints
|
|
5
|
+
// (search, preview, count, scroll). If only `/search` is affected, the fix is narrower.
|
|
6
|
+
// 2. Top-level shape works consistently — filter combinations (query + country,
|
|
7
|
+
// query + employees, etc.) all narrow the result set as expected.
|
|
8
|
+
// 3. The bulk-enrich endpoint is unaffected (the bug is search-specific).
|
|
9
|
+
// 4. Ranges that wrapFilters translates (employees, foundedYear) still need their
|
|
10
|
+
// translation logic — confirm whether upstream wants employeesFrom/employeesTo top-level
|
|
11
|
+
// or {employees: {from, to}} or [{from, to}].
|
|
12
|
+
//
|
|
13
|
+
// Run: COMPANYENRICH_API_KEY=… npx tsx mcp/tests/live/companyenrich-fix-validation.ts
|
|
14
|
+
|
|
15
|
+
const KEY = process.env.COMPANYENRICH_API_KEY
|
|
16
|
+
if (!KEY) { console.error('COMPANYENRICH_API_KEY required'); process.exit(1) }
|
|
17
|
+
const BASE = 'https://api.companyenrich.com'
|
|
18
|
+
|
|
19
|
+
async function callRaw(path: string, body: unknown) {
|
|
20
|
+
const t0 = Date.now()
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal: AbortSignal.timeout(30_000),
|
|
27
|
+
})
|
|
28
|
+
let data: any
|
|
29
|
+
try { data = await res.json() } catch { data = '<non-json>' }
|
|
30
|
+
return { ok: res.ok, status: res.status, ms: Date.now() - t0, data }
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { ok: false, status: 0, ms: Date.now() - t0, data: { error: String(err) } }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function summarize(r: any) {
|
|
37
|
+
const items = (r.data?.items ?? []).slice(0, 3).map((c: any) => `${c.name}/${c.country ?? '?'}`)
|
|
38
|
+
return `status=${r.status} ms=${r.ms} count=${r.data?.items?.length ?? r.data?.count ?? '?'} top=${JSON.stringify(items)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function header(s: string) { console.log(`\n${'═'.repeat(78)}\n${s}\n${'═'.repeat(78)}`) }
|
|
42
|
+
function sub(s: string) { console.log(`\n— ${s}`) }
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
// ============================================================================
|
|
46
|
+
header('PART 1 — Confirm the wrap-vs-top-level bug on EACH of 4 search endpoints')
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
const wrapped = { pageSize: 5, filters: { query: 'wework' } }
|
|
50
|
+
const flat = { pageSize: 5, query: 'wework' }
|
|
51
|
+
|
|
52
|
+
for (const path of ['/companies/search', '/companies/search/preview', '/companies/search/scroll']) {
|
|
53
|
+
sub(`${path} wrapped (current)`)
|
|
54
|
+
console.log(' ', summarize(await callRaw(path, wrapped)))
|
|
55
|
+
sub(`${path} flat (proposed)`)
|
|
56
|
+
console.log(' ', summarize(await callRaw(path, flat)))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// count endpoint returns a count not items
|
|
60
|
+
sub('/companies/search/count wrapped (current)')
|
|
61
|
+
let r = await callRaw('/companies/search/count', { filters: { query: 'wework' } })
|
|
62
|
+
console.log(` status=${r.status} ms=${r.ms} data=${JSON.stringify(r.data).slice(0, 200)}`)
|
|
63
|
+
sub('/companies/search/count flat (proposed)')
|
|
64
|
+
r = await callRaw('/companies/search/count', { query: 'wework' })
|
|
65
|
+
console.log(` status=${r.status} ms=${r.ms} data=${JSON.stringify(r.data).slice(0, 200)}`)
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
header('PART 2 — Confirm flat-shape combinations actually narrow results')
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
sub('flat: query="wework" + countries=["BR"]')
|
|
72
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, query: 'wework', countries: ['BR'] })))
|
|
73
|
+
|
|
74
|
+
sub('flat: query="wework" + countries=["US"]')
|
|
75
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, query: 'wework', countries: ['US'] })))
|
|
76
|
+
|
|
77
|
+
sub('flat: countries=["BR"] + keywords=["fintech"]')
|
|
78
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, countries: ['BR'], keywords: ['fintech'] })))
|
|
79
|
+
|
|
80
|
+
sub('flat: keywords=["SaaS"] (does keywords field do anything at top level?)')
|
|
81
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, keywords: ['SaaS'] })))
|
|
82
|
+
|
|
83
|
+
sub('flat: industries=["Software"] (does industries field do anything?)')
|
|
84
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, industries: ['Software'] })))
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
header('PART 3 — Confirm range shape (employees, foundedYear) works at top level')
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
sub('flat: employees=[{from:1000, to:5000}]')
|
|
91
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, employees: [{ from: 1000, to: 5000 }] })))
|
|
92
|
+
|
|
93
|
+
sub('flat: employeesFrom=1000 + employeesTo=5000 (the wrapFilters translation target)')
|
|
94
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, employeesFrom: 1000, employeesTo: 5000 })))
|
|
95
|
+
|
|
96
|
+
sub('flat: foundedYear={from:2015, to:2020}')
|
|
97
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, foundedYear: { from: 2015, to: 2020 } })))
|
|
98
|
+
|
|
99
|
+
sub('flat: foundedFrom=2015 + foundedTo=2020 (translated form)')
|
|
100
|
+
console.log(' ', summarize(await callRaw('/companies/search', { pageSize: 5, foundedFrom: 2015, foundedTo: 2020 })))
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
header('PART 4 — Confirm bulk-enrich endpoint is unaffected (no wrapFilters there)')
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
sub('POST /companies/enrich?domain=wework.com (single enrich, GET-like)')
|
|
107
|
+
// Single enrich uses query params, not wrapFilters — should be unaffected.
|
|
108
|
+
try {
|
|
109
|
+
const er = await fetch(`${BASE}/companies/enrich?domain=wework.com`, {
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: { Authorization: `Bearer ${KEY}` },
|
|
112
|
+
signal: AbortSignal.timeout(20_000),
|
|
113
|
+
})
|
|
114
|
+
const ed = await er.json()
|
|
115
|
+
console.log(` status=${er.status} name=${(ed as any)?.name} domain=${(ed as any)?.domain}`)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log(` failed: ${err}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
header('DONE')
|
|
122
|
+
// ============================================================================
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch((e) => { console.error('FATAL', e); process.exit(1) })
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Hybrid: scalars top-level, ranges wrapped? Or ranges with a special shape?
|
|
2
|
+
const KEY = process.env.COMPANYENRICH_API_KEY!
|
|
3
|
+
const BASE = 'https://api.companyenrich.com'
|
|
4
|
+
|
|
5
|
+
async function count(label: string, body: unknown) {
|
|
6
|
+
const t0 = Date.now()
|
|
7
|
+
const res = await fetch(`${BASE}/companies/search/count`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
|
10
|
+
body: JSON.stringify(body),
|
|
11
|
+
signal: AbortSignal.timeout(20_000),
|
|
12
|
+
})
|
|
13
|
+
const data: any = await res.json().catch(() => ({}))
|
|
14
|
+
console.log(`[${label}]\n body=${JSON.stringify(body)}\n status=${res.status} ms=${Date.now() - t0} count=${data?.count} total=${data?.total}\n`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
// Hypothesis A: maybe ranges need `filters: {}` wrapping (hybrid model)
|
|
19
|
+
await count('A1: countries top-level + filters.employees', {
|
|
20
|
+
countries: ['US'], filters: { employees: { from: 1000, to: 5000 } },
|
|
21
|
+
})
|
|
22
|
+
await count('A2: countries top-level + filters.employeesFrom/To', {
|
|
23
|
+
countries: ['US'], filters: { employeesFrom: 1000, employeesTo: 5000 },
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Hypothesis B: everything inside filters (back to old shape)
|
|
27
|
+
await count('B: filters.countries + filters.employees array', {
|
|
28
|
+
filters: { countries: ['US'], employees: [{ from: 1000, to: 5000 }] },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Hypothesis C: maybe a different field name (size, headcount_range, employee_range)
|
|
32
|
+
await count('C1: size {from,to}', { size: { from: 1000, to: 5000 } })
|
|
33
|
+
await count('C2: headcountRange {min,max}', { headcountRange: { min: 1000, max: 5000 } })
|
|
34
|
+
await count('C3: employeesRange', { employeesRange: { from: 1000, to: 5000 } })
|
|
35
|
+
|
|
36
|
+
// Hypothesis D: maybe array of buckets like ["1001-5000"] or letter codes
|
|
37
|
+
await count('D1: employees ["1001-5000"]', { employees: ['1001-5000'] })
|
|
38
|
+
await count('D2: employees ["F"]', { employees: ['F'] }) // LimaData uses letter buckets, just in case
|
|
39
|
+
|
|
40
|
+
// Hypothesis E: snake_case field name
|
|
41
|
+
await count('E: employees_from/employees_to', { employees_from: 1000, employees_to: 5000 })
|
|
42
|
+
|
|
43
|
+
// Hypothesis F: revenue & funding to triangulate — do ANY ranges work?
|
|
44
|
+
await count('F1: revenueFrom/revenueTo', { revenueFrom: 1000000, revenueTo: 10000000 })
|
|
45
|
+
await count('F2: filters.revenueFrom/revenueTo', { filters: { revenueFrom: 1000000, revenueTo: 10000000 } })
|
|
46
|
+
|
|
47
|
+
// Hypothesis G: lookup an actual reference-data endpoint for valid bucket IDs (CompanyEnrich does have one)
|
|
48
|
+
// Skipping — adds latency; do if all of the above fail.
|
|
49
|
+
|
|
50
|
+
// Sanity check
|
|
51
|
+
await count('sanity: countries=DE alone', { countries: ['DE'] })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Probe CompanyEnrich /companies/search to find the correct field name for brand-name lookup.
|
|
2
|
+
// We hit our marketplace endpoint with different request shapes and observe what comes back.
|
|
3
|
+
const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
|
|
4
|
+
const API_KEY = process.env.COLDIQ_API_KEY!
|
|
5
|
+
|
|
6
|
+
async function call(body: unknown) {
|
|
7
|
+
const t0 = Date.now()
|
|
8
|
+
const res = await fetch(`${API_URL}/v1/companyenrich/companies/search`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
signal: AbortSignal.timeout(30_000),
|
|
13
|
+
})
|
|
14
|
+
let data: any
|
|
15
|
+
try { data = await res.json() } catch { data = '<non-json>' }
|
|
16
|
+
return { ok: res.ok, status: res.status, ms: Date.now() - t0, data }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function summarize(r: any) {
|
|
20
|
+
const items = (r.data?.items ?? []).slice(0, 5).map((c: any) => `${c.name} (${c.domain})`)
|
|
21
|
+
return `status=${r.status} ms=${r.ms} items=${items.length} top5=${JSON.stringify(items)}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
console.log('=== Probe: how to find WeWork in CompanyEnrich ===\n')
|
|
26
|
+
|
|
27
|
+
console.log('1) query="wework"')
|
|
28
|
+
console.log(' ', summarize(await call({ query: 'wework', pageSize: 5 })))
|
|
29
|
+
|
|
30
|
+
console.log('\n2) names=["WeWork"]')
|
|
31
|
+
console.log(' ', summarize(await call({ names: ['WeWork'], pageSize: 5 })))
|
|
32
|
+
|
|
33
|
+
console.log('\n3) domains=["wework.com"]')
|
|
34
|
+
console.log(' ', summarize(await call({ domains: ['wework.com'], pageSize: 5 })))
|
|
35
|
+
|
|
36
|
+
console.log('\n4) query="wework" + countries=["US"]')
|
|
37
|
+
console.log(' ', summarize(await call({ query: 'wework', countries: ['US'], pageSize: 5 })))
|
|
38
|
+
|
|
39
|
+
console.log('\n5) semanticQuery="WeWork coworking"')
|
|
40
|
+
console.log(' ', summarize(await call({ semanticQuery: 'WeWork coworking', pageSize: 5 })))
|
|
41
|
+
|
|
42
|
+
console.log('\n6) names=["wework"] (lowercase)')
|
|
43
|
+
console.log(' ', summarize(await call({ names: ['wework'], pageSize: 5 })))
|
|
44
|
+
|
|
45
|
+
console.log('\n7) only countries=["BR"] (baseline — what does no name filter return?)')
|
|
46
|
+
console.log(' ', summarize(await call({ countries: ['BR'], pageSize: 5 })))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Probe the range-filter shape. We use /companies/search/count because it gives a
|
|
2
|
+
// definitive "did the filter narrow?" signal — full-DB count is ~20.4M, so anything
|
|
3
|
+
// less means the filter applied.
|
|
4
|
+
const KEY = process.env.COMPANYENRICH_API_KEY!
|
|
5
|
+
const BASE = 'https://api.companyenrich.com'
|
|
6
|
+
|
|
7
|
+
async function count(label: string, body: unknown) {
|
|
8
|
+
const t0 = Date.now()
|
|
9
|
+
const res = await fetch(`${BASE}/companies/search/count`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify(body),
|
|
13
|
+
signal: AbortSignal.timeout(20_000),
|
|
14
|
+
})
|
|
15
|
+
const data: any = await res.json().catch(() => ({}))
|
|
16
|
+
const ms = Date.now() - t0
|
|
17
|
+
console.log(`[${label}]`)
|
|
18
|
+
console.log(` body=${JSON.stringify(body)}`)
|
|
19
|
+
console.log(` status=${res.status} ms=${ms} count=${data?.count} total=${data?.total}\n`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
// Baselines
|
|
24
|
+
await count('baseline empty', {})
|
|
25
|
+
await count('baseline countries=US', { countries: ['US'] })
|
|
26
|
+
|
|
27
|
+
// Range candidates — try every shape
|
|
28
|
+
await count('flat employeesFrom/employeesTo', { employeesFrom: 1000, employeesTo: 5000 })
|
|
29
|
+
await count('flat employees array [{from,to}]', { employees: [{ from: 1000, to: 5000 }] })
|
|
30
|
+
await count('flat employees object {from,to}', { employees: { from: 1000, to: 5000 } })
|
|
31
|
+
await count('flat employees scalar {min,max}', { employees: { min: 1000, max: 5000 } })
|
|
32
|
+
await count('flat employeesMin/employeesMax', { employeesMin: 1000, employeesMax: 5000 })
|
|
33
|
+
await count('flat headcount', { headcount: { from: 1000, to: 5000 } })
|
|
34
|
+
await count('flat employee_count', { employee_count: { from: 1000, to: 5000 } })
|
|
35
|
+
|
|
36
|
+
// Ranges combined with a primary filter (in case they need company narrowing first)
|
|
37
|
+
await count('countries=US + employeesFrom/To', { countries: ['US'], employeesFrom: 1000, employeesTo: 5000 })
|
|
38
|
+
await count('countries=US + employees array', { countries: ['US'], employees: [{ from: 1000, to: 5000 }] })
|
|
39
|
+
|
|
40
|
+
// Founded year shapes
|
|
41
|
+
await count('foundedFrom/foundedTo', { foundedFrom: 2015, foundedTo: 2020 })
|
|
42
|
+
await count('foundedYear {from,to}', { foundedYear: { from: 2015, to: 2020 } })
|
|
43
|
+
await count('foundedYearFrom/foundedYearTo', { foundedYearFrom: 2015, foundedYearTo: 2020 })
|
|
44
|
+
|
|
45
|
+
// Sanity — does a known-working filter still narrow alongside ranges?
|
|
46
|
+
await count('countries=BR (sanity)', { countries: ['BR'] })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main()
|