@coldiq/mcp 0.3.8 → 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.
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +58 -17
- package/dist/registry.js.map +1 -1
- package/dist/tools/search-companies.d.ts +2 -1
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +10 -3
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/utils/compact-companies.d.ts +21 -0
- package/dist/utils/compact-companies.d.ts.map +1 -0
- package/dist/utils/compact-companies.js +198 -0
- package/dist/utils/compact-companies.js.map +1 -0
- package/package.json +1 -1
- package/src/registry.ts +58 -17
- package/src/tools/search-companies.ts +10 -3
- package/src/utils/compact-companies.ts +208 -0
- package/tests/live/companyenrich-keyword-tag-probe.ts +14 -3
- package/tests/registry-polling.test.ts +9 -3
- package/tests/registry-search-companies.test.ts +32 -5
- package/tests/registry.test.ts +33 -11
- package/tests/tools/search-companies.test.ts +78 -2
- package/tests/utils/compact-companies.test.ts +146 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { executeWithFallback, isExecutionError } from '../executor.js';
|
|
3
3
|
import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js';
|
|
4
|
+
import { compactCompaniesPayload } from '../utils/compact-companies.js';
|
|
4
5
|
export const searchCompaniesName = 'search_companies';
|
|
5
|
-
export const searchCompaniesDescription = 'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. For buying-intent topic discovery (find companies associated with intent keywords like "sales automation" or "lead generation"), pass `keywords` with `use_providers: ["theirstack"]` — this routes to TheirStack\'s intent-keyword index instead of the default full-text match. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25.'
|
|
6
|
+
export const searchCompaniesDescription = 'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. For buying-intent topic discovery (find companies associated with intent keywords like "sales automation" or "lead generation"), pass `keywords` with `use_providers: ["theirstack"]` — this routes to TheirStack\'s intent-keyword index instead of the default full-text match. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
7
|
+
'Response is compact by default (name, domain, website, linkedin_url, employees, revenue, industry, country, city, founded_year, categories) — everything needed to act on a company or feed it into find_people. Set fields="verbose" only when you specifically need the full firmographic record (funding history, technologies, keywords, NAICS codes, all socials, descriptions).';
|
|
6
8
|
export const searchCompaniesSchema = {
|
|
7
9
|
keywords: z.array(z.string()).optional().describe('Free-text topics, business models, or themes — e.g. ["SaaS", "fintech", "cybersecurity"]. Best for most discovery: matched broadly across company name, description, and tags. Prefer this over `industries` for terms like "SaaS" that are business models rather than formal industry categories.'),
|
|
8
10
|
countries: z.array(z.string()).optional().describe('2-letter ISO country codes (e.g. ["FR", "US"])'),
|
|
@@ -27,10 +29,12 @@ export const searchCompaniesSchema = {
|
|
|
27
29
|
min_workforce_growth_pct: z.number().optional().describe('Minimum workforce growth % over the past 12 months (e.g. 10 for 10%)'),
|
|
28
30
|
linkedin_search_url: z.string().optional().describe('LinkedIn Sales Navigator company search URL — when provided, enables URL-based prospect discovery as a final fallback. Most users should leave this unset.'),
|
|
29
31
|
limit: z.number().min(1).max(500).default(25).describe('Max results to return (default: 25, max: 500). Pulls above 100 auto-paginate the underlying provider; on Apollo each 100 companies costs 1 credit, billed only for pages actually returned.'),
|
|
32
|
+
fields: z.enum(['compact', 'verbose']).default('compact').describe('Response shape. "compact" (default) returns a small per-company record: name, domain, website, linkedin_url, employees, revenue, industry, country, city, founded_year, categories — plus total. "verbose" returns the raw provider passthrough (funding-round history, technologies, keyword arrays, NAICS codes, all socials, descriptions) — only use when you specifically need those fields.'),
|
|
30
33
|
use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_companies').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
31
34
|
};
|
|
32
35
|
export async function searchCompaniesHandler(input) {
|
|
33
|
-
const { use_providers: rawUseProviders, ...restInput } = input;
|
|
36
|
+
const { use_providers: rawUseProviders, fields: rawFields, ...restInput } = input;
|
|
37
|
+
const fields = rawFields === 'verbose' ? 'verbose' : 'compact';
|
|
34
38
|
const resolved = resolvePreferredProviders('search_companies', restInput, rawUseProviders);
|
|
35
39
|
if (!resolved.ok) {
|
|
36
40
|
return { content: [{ type: 'text', text: JSON.stringify(resolved.error) }], isError: true };
|
|
@@ -46,6 +50,9 @@ export async function searchCompaniesHandler(input) {
|
|
|
46
50
|
;
|
|
47
51
|
result._meta.total_entries = totalEntries;
|
|
48
52
|
}
|
|
49
|
-
|
|
53
|
+
const payload = fields === 'compact'
|
|
54
|
+
? { ...result, data: compactCompaniesPayload(result.data) }
|
|
55
|
+
: result;
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
50
57
|
}
|
|
51
58
|
//# sourceMappingURL=search-companies.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-companies.js","sourceRoot":"","sources":["../../src/tools/search-companies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACtE,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"search-companies.js","sourceRoot":"","sources":["../../src/tools/search-companies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CompactCompany {
|
|
2
|
+
name?: string;
|
|
3
|
+
domain?: string;
|
|
4
|
+
website?: string;
|
|
5
|
+
linkedin_url?: string;
|
|
6
|
+
employees?: number | string;
|
|
7
|
+
revenue?: number | string;
|
|
8
|
+
industry?: string;
|
|
9
|
+
country?: string;
|
|
10
|
+
city?: string;
|
|
11
|
+
founded_year?: number | string;
|
|
12
|
+
categories?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function extractCompaniesArray(data: unknown): unknown[];
|
|
15
|
+
export declare function normalizeCompany(record: unknown): CompactCompany | null;
|
|
16
|
+
export interface CompactCompaniesPayload {
|
|
17
|
+
companies: CompactCompany[];
|
|
18
|
+
total?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function compactCompaniesPayload(data: unknown): CompactCompaniesPayload;
|
|
21
|
+
//# sourceMappingURL=compact-companies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-companies.d.ts","sourceRoot":"","sources":["../../src/utils/compact-companies.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB;AA2DD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,CAmB9D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CA4EvE;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,uBAAuB,CAkB9E"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Normalize search_companies responses into a small, predictable shape.
|
|
2
|
+
//
|
|
3
|
+
// The search_companies waterfall fans across many providers, each with a
|
|
4
|
+
// different payload shape (CompanyEnrich nests at data.items[], Apollo at
|
|
5
|
+
// data.organizations[] + pagination, FullEnrich at data.companies[], others at
|
|
6
|
+
// data.data[]/results[]). The raw provider object per company is heavy
|
|
7
|
+
// (~3-4KB: full funding-round history, technologies, ~30-item keyword arrays,
|
|
8
|
+
// NAICS codes, every social URL, SEO descriptions, logos). Compact mode keeps
|
|
9
|
+
// only the fields agents act on downstream — company identity (name, domain,
|
|
10
|
+
// website, linkedin_url, used by find_people/find_emails) plus the firmographic
|
|
11
|
+
// signals a user filters on (employees, revenue, industry, country, city,
|
|
12
|
+
// founded_year, categories). It changes how many descriptive fields ride along,
|
|
13
|
+
// never which companies are returned. Verbose mode keeps the raw passthrough.
|
|
14
|
+
function getPath(obj, path) {
|
|
15
|
+
if (obj == null)
|
|
16
|
+
return undefined;
|
|
17
|
+
let cur = obj;
|
|
18
|
+
for (const part of path.split('.')) {
|
|
19
|
+
if (cur == null)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (Array.isArray(cur)) {
|
|
22
|
+
const idx = Number(part);
|
|
23
|
+
if (!Number.isInteger(idx))
|
|
24
|
+
return undefined;
|
|
25
|
+
cur = cur[idx];
|
|
26
|
+
}
|
|
27
|
+
else if (typeof cur === 'object') {
|
|
28
|
+
cur = cur[part];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return cur;
|
|
35
|
+
}
|
|
36
|
+
function pick(obj, paths) {
|
|
37
|
+
for (const p of paths) {
|
|
38
|
+
const v = getPath(obj, p);
|
|
39
|
+
if (v !== undefined && v !== null && v !== '')
|
|
40
|
+
return v;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function asString(v) {
|
|
45
|
+
if (typeof v === 'string')
|
|
46
|
+
return v.length > 0 ? v : undefined;
|
|
47
|
+
if (typeof v === 'number')
|
|
48
|
+
return String(v);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function asStringOrNumber(v) {
|
|
52
|
+
if (typeof v === 'number')
|
|
53
|
+
return v;
|
|
54
|
+
if (typeof v === 'string')
|
|
55
|
+
return v.length > 0 ? v : undefined;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function isLinkedInUrl(v) {
|
|
59
|
+
if (typeof v !== 'string' || v.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
return v.toLowerCase().includes('linkedin.com/');
|
|
62
|
+
}
|
|
63
|
+
function pickLinkedIn(obj, paths) {
|
|
64
|
+
for (const p of paths) {
|
|
65
|
+
const v = getPath(obj, p);
|
|
66
|
+
if (isLinkedInUrl(v))
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
// Probe the keys providers use to hold the companies array. Covers the flat
|
|
72
|
+
// shapes (CompanyEnrich `items`, FullEnrich/BlitzAPI `companies`, Apollo
|
|
73
|
+
// `organizations`, generic `data`/`results`, AI-Ark `content`) and the one-level
|
|
74
|
+
// nested shape LinkUp uses (`data.companies[]`). Keys are kept in sync with the
|
|
75
|
+
// `hasResult` checks in registry.ts so compact never returns [] for a provider
|
|
76
|
+
// that actually matched.
|
|
77
|
+
export function extractCompaniesArray(data) {
|
|
78
|
+
if (data == null)
|
|
79
|
+
return [];
|
|
80
|
+
if (Array.isArray(data))
|
|
81
|
+
return data;
|
|
82
|
+
if (typeof data !== 'object')
|
|
83
|
+
return [];
|
|
84
|
+
const d = data;
|
|
85
|
+
for (const key of ['items', 'companies', 'organizations', 'data', 'results', 'content']) {
|
|
86
|
+
const v = d[key];
|
|
87
|
+
if (Array.isArray(v))
|
|
88
|
+
return v;
|
|
89
|
+
}
|
|
90
|
+
// LinkUp (search / fundraising / hiring) wraps results under data.companies[].
|
|
91
|
+
const inner = d.data;
|
|
92
|
+
if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
|
|
93
|
+
const di = inner;
|
|
94
|
+
for (const key of ['companies', 'organizations', 'results', 'items', 'content']) {
|
|
95
|
+
const v = di[key];
|
|
96
|
+
if (Array.isArray(v))
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
export function normalizeCompany(record) {
|
|
103
|
+
if (!record || typeof record !== 'object' || Array.isArray(record))
|
|
104
|
+
return null;
|
|
105
|
+
const r = record;
|
|
106
|
+
const out = {};
|
|
107
|
+
const name = asString(pick(r, ['name', 'company_name', 'organization.name', 'companyName']));
|
|
108
|
+
if (name)
|
|
109
|
+
out.name = name;
|
|
110
|
+
const domain = asString(pick(r, ['domain', 'primary_domain', 'website_domain', 'organization.primary_domain']));
|
|
111
|
+
if (domain)
|
|
112
|
+
out.domain = domain;
|
|
113
|
+
const website = asString(pick(r, ['website', 'website_url', 'organization.website_url', 'url']));
|
|
114
|
+
if (website)
|
|
115
|
+
out.website = website;
|
|
116
|
+
// LinkedIn URL is strict-validated: a wrong value silently breaks downstream
|
|
117
|
+
// find_people LinkedIn-driven lookups.
|
|
118
|
+
const linkedin = pickLinkedIn(r, [
|
|
119
|
+
'socials.linkedin_url',
|
|
120
|
+
'linkedin_url',
|
|
121
|
+
'organization.linkedin_url',
|
|
122
|
+
'social_profiles.linkedin.url',
|
|
123
|
+
]);
|
|
124
|
+
if (linkedin)
|
|
125
|
+
out.linkedin_url = linkedin;
|
|
126
|
+
const employees = asStringOrNumber(pick(r, [
|
|
127
|
+
'employees',
|
|
128
|
+
'employee_count',
|
|
129
|
+
'employees_on_linkedin', // BlitzAPI
|
|
130
|
+
'estimated_num_employees',
|
|
131
|
+
'num_employees',
|
|
132
|
+
'organization.estimated_num_employees',
|
|
133
|
+
'size',
|
|
134
|
+
'headcount',
|
|
135
|
+
]));
|
|
136
|
+
if (employees !== undefined)
|
|
137
|
+
out.employees = employees;
|
|
138
|
+
const revenue = asStringOrNumber(pick(r, [
|
|
139
|
+
'revenue',
|
|
140
|
+
'estimated_revenue',
|
|
141
|
+
'annual_revenue',
|
|
142
|
+
'organization.annual_revenue',
|
|
143
|
+
]));
|
|
144
|
+
if (revenue !== undefined)
|
|
145
|
+
out.revenue = revenue;
|
|
146
|
+
const industry = asString(pick(r, ['industry', 'organization.industry']));
|
|
147
|
+
if (industry)
|
|
148
|
+
out.industry = industry;
|
|
149
|
+
const country = asString(pick(r, [
|
|
150
|
+
'location.country.name',
|
|
151
|
+
'location.country.code',
|
|
152
|
+
'country',
|
|
153
|
+
'organization.country',
|
|
154
|
+
'location_country',
|
|
155
|
+
]));
|
|
156
|
+
if (country)
|
|
157
|
+
out.country = country;
|
|
158
|
+
const city = asString(pick(r, [
|
|
159
|
+
'location.city.name',
|
|
160
|
+
'city',
|
|
161
|
+
'organization.city',
|
|
162
|
+
'location_city',
|
|
163
|
+
]));
|
|
164
|
+
if (city)
|
|
165
|
+
out.city = city;
|
|
166
|
+
const foundedYear = asStringOrNumber(pick(r, ['founded_year', 'organization.founded_year', 'foundation_date']));
|
|
167
|
+
if (foundedYear !== undefined)
|
|
168
|
+
out.founded_year = foundedYear;
|
|
169
|
+
const categories = getPath(r, 'categories');
|
|
170
|
+
if (Array.isArray(categories)) {
|
|
171
|
+
const cats = categories.filter((c) => typeof c === 'string' && c.length > 0);
|
|
172
|
+
if (cats.length > 0)
|
|
173
|
+
out.categories = cats;
|
|
174
|
+
}
|
|
175
|
+
// Drop records that resolved to nothing meaningful.
|
|
176
|
+
if (Object.keys(out).length === 0)
|
|
177
|
+
return null;
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
export function compactCompaniesPayload(data) {
|
|
181
|
+
const arr = extractCompaniesArray(data);
|
|
182
|
+
const companies = [];
|
|
183
|
+
for (const r of arr) {
|
|
184
|
+
const c = normalizeCompany(r);
|
|
185
|
+
if (c)
|
|
186
|
+
companies.push(c);
|
|
187
|
+
}
|
|
188
|
+
const out = { companies };
|
|
189
|
+
// Preserve the total-addressable-market count when the provider exposes one
|
|
190
|
+
// (CompanyEnrich: data.totalItems; Apollo: data.pagination.total_entries).
|
|
191
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
192
|
+
const total = pick(data, ['totalItems', 'total', 'pagination.total_entries', 'pagination.total']);
|
|
193
|
+
if (typeof total === 'number')
|
|
194
|
+
out.total = total;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=compact-companies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-companies.js","sourceRoot":"","sources":["../../src/utils/compact-companies.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/registry.ts
CHANGED
|
@@ -312,6 +312,36 @@ function mapEmployeesToLimaDataHeadcount(min?: number, max?: number): string[] {
|
|
|
312
312
|
return buckets.filter((b) => b.max >= lo && b.min <= hi).map((b) => b.code)
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// CompanyEnrich's `category` filter is a controlled vocabulary matched against a
|
|
316
|
+
// company's clean `categories` classification (NOT its free-text keyword tags).
|
|
317
|
+
// These are the only accepted values — verified empirically against the live count
|
|
318
|
+
// API, where any value outside this set returns an empty result set (see
|
|
319
|
+
// mcp/tests/live/companyenrich-keyword-tag-probe.ts).
|
|
320
|
+
const COMPANYENRICH_CATEGORIES = new Set([
|
|
321
|
+
'b2b', 'b2c', 'b2g', 'saas', 'service-provider', 'media', 'e-commerce', 'mobile',
|
|
322
|
+
])
|
|
323
|
+
|
|
324
|
+
// Split discovery terms into (a) controlled categories routed to the precise
|
|
325
|
+
// `category` filter and (b) free-text themes routed to the forgiving `keywords`
|
|
326
|
+
// tag filter. ANDing categories means "is classified b2b AND is classified saas"
|
|
327
|
+
// — what "B2B SaaS" actually means. ANDing the noisy keyword *text* tags instead
|
|
328
|
+
// would concentrate agencies whose copy merely name-drops both terms, while
|
|
329
|
+
// free-text themes like "fintech" are rejected outright by the category filter.
|
|
330
|
+
function splitCompanyEnrichTerms(terms: string[]): { categories: string[]; keywords: string[] } {
|
|
331
|
+
const categories: string[] = []
|
|
332
|
+
const keywords: string[] = []
|
|
333
|
+
for (const raw of terms) {
|
|
334
|
+
const slug = raw.trim().toLowerCase().replace(/\s+/g, '-')
|
|
335
|
+
const canonical = slug === 'ecommerce' ? 'e-commerce' : slug
|
|
336
|
+
if (COMPANYENRICH_CATEGORIES.has(canonical)) {
|
|
337
|
+
if (!categories.includes(canonical)) categories.push(canonical)
|
|
338
|
+
} else {
|
|
339
|
+
keywords.push(raw)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { categories, keywords }
|
|
343
|
+
}
|
|
344
|
+
|
|
315
345
|
const searchCompaniesProviders: ProviderEntry[] = [
|
|
316
346
|
{
|
|
317
347
|
id: 'companyenrich',
|
|
@@ -319,22 +349,25 @@ const searchCompaniesProviders: ProviderEntry[] = [
|
|
|
319
349
|
method: 'POST',
|
|
320
350
|
priority: 1,
|
|
321
351
|
mapParams: (input) => {
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
352
|
+
// Hybrid routing for free-text discovery terms. CompanyEnrich exposes two
|
|
353
|
+
// distinct matchers and the difference is decisive:
|
|
354
|
+
// - `category`: a controlled vocabulary (b2b/b2c/b2g/saas/...) matched on a
|
|
355
|
+
// company's clean classification. ANDing these gives a precise "is B2B AND
|
|
356
|
+
// is SaaS" filter (10/10 real SaaS companies in live tests).
|
|
357
|
+
// - `keywords`: a forgiving tag filter over description-derived text tags.
|
|
358
|
+
// Right for open themes (fintech, cybersecurity) that have no category.
|
|
359
|
+
// We must NOT route everything to `keywords`: ANDing text tags concentrates
|
|
360
|
+
// agencies that merely name-drop "saas"/"b2b" in their copy, and `query`
|
|
361
|
+
// (the original mapping) matched company NAME + domain, surfacing firms merely
|
|
362
|
+
// *named* "SAAS Partners". So we split: vocabulary terms -> `category` (And),
|
|
363
|
+
// everything else -> `keywords` (Or). Industries are folded in because
|
|
364
|
+
// CompanyEnrich's `industries` field needs exact taxonomy matches and silently
|
|
365
|
+
// ignores free-text like "SaaS". Empirically validated against the live
|
|
366
|
+
// count/search API — see mcp/tests/live/companyenrich-keyword-tag-probe.ts.
|
|
367
|
+
const { categories: categoryTerms, keywords: keywordTags } = splitCompanyEnrichTerms([
|
|
335
368
|
...((input.keywords as string[] | undefined) ?? []),
|
|
336
369
|
...((input.industries as string[] | undefined) ?? []),
|
|
337
|
-
]
|
|
370
|
+
])
|
|
338
371
|
const excludeObj: Record<string, unknown> = {}
|
|
339
372
|
if (isNonEmptyArray(input.exclude_domains)) excludeObj.domains = input.exclude_domains
|
|
340
373
|
if (isNonEmptyArray(input.exclude_industries)) excludeObj.industries = input.exclude_industries
|
|
@@ -342,6 +375,8 @@ const searchCompaniesProviders: ProviderEntry[] = [
|
|
|
342
375
|
return {
|
|
343
376
|
body: {
|
|
344
377
|
countries: input.countries,
|
|
378
|
+
category: categoryTerms.length > 0 ? categoryTerms : undefined,
|
|
379
|
+
categoryOperator: categoryTerms.length > 0 ? 'And' : undefined,
|
|
345
380
|
keywords: keywordTags.length > 0 ? keywordTags : undefined,
|
|
346
381
|
keywordsOperator: keywordTags.length > 0 ? 'Or' : undefined,
|
|
347
382
|
technologies: isNonEmptyArray(input.technologies) ? input.technologies : undefined,
|
|
@@ -1144,10 +1179,16 @@ const findPeopleProviders: ProviderEntry[] = [
|
|
|
1144
1179
|
},
|
|
1145
1180
|
async: {
|
|
1146
1181
|
pollEndpoint: (id: string) => `/leadsfactory/contact-finder/searches/${id}`,
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1182
|
+
// Fast early probes, then a flat 12s ceiling. LeadsFactory streams results
|
|
1183
|
+
// for minutes; the flat ceiling bounds completion-detection lag to ≤12s
|
|
1184
|
+
// (the old unbounded +8s/attempt growth reached 44–72s gaps, so a search
|
|
1185
|
+
// that finished mid-interval sat unnoticed for up to ~a minute). Poll
|
|
1186
|
+
// responses aren't billed and 12s is gentler than the 2s/5s front probes,
|
|
1187
|
+
// so this only removes idle waiting — the returned data is unchanged (we
|
|
1188
|
+
// still only return once status flips to SUCCESSFUL/FAILED).
|
|
1189
|
+
// Schedule: 2s, 5s, then 12s flat.
|
|
1149
1190
|
pollIntervalMs: (attempt) =>
|
|
1150
|
-
attempt === 1 ? 2000 : attempt === 2 ? 5000 : 12000
|
|
1191
|
+
attempt === 1 ? 2000 : attempt === 2 ? 5000 : 12000,
|
|
1151
1192
|
timeoutMs: 300_000, // 5 minutes
|
|
1152
1193
|
isComplete: (data) => {
|
|
1153
1194
|
const d = data as Record<string, unknown>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { executeWithFallback, isExecutionError } from '../executor.js'
|
|
3
3
|
import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
|
+
import { compactCompaniesPayload } from '../utils/compact-companies.js'
|
|
4
5
|
|
|
5
6
|
export const searchCompaniesName = 'search_companies'
|
|
6
7
|
|
|
7
8
|
export const searchCompaniesDescription =
|
|
8
|
-
'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. For buying-intent topic discovery (find companies associated with intent keywords like "sales automation" or "lead generation"), pass `keywords` with `use_providers: ["theirstack"]` — this routes to TheirStack\'s intent-keyword index instead of the default full-text match. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25.'
|
|
9
|
+
'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. Routing is automatic: strict firmographic filters (revenue, employee bands, exclusion lists) get high-precision providers first; tech-stack and funding-stage filters route to specialized providers; loose keyword/geo searches fall back to broad providers; a LinkedIn Sales Navigator search URL enables URL-based discovery as a final fallback. For buying-intent topic discovery (find companies associated with intent keywords like "sales automation" or "lead generation"), pass `keywords` with `use_providers: ["theirstack"]` — this routes to TheirStack\'s intent-keyword index instead of the default full-text match. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
10
|
+
'Response is compact by default (name, domain, website, linkedin_url, employees, revenue, industry, country, city, founded_year, categories) — everything needed to act on a company or feed it into find_people. Set fields="verbose" only when you specifically need the full firmographic record (funding history, technologies, keywords, NAICS codes, all socials, descriptions).'
|
|
9
11
|
|
|
10
12
|
export const searchCompaniesSchema = {
|
|
11
13
|
keywords: z.array(z.string()).optional().describe('Free-text topics, business models, or themes — e.g. ["SaaS", "fintech", "cybersecurity"]. Best for most discovery: matched broadly across company name, description, and tags. Prefer this over `industries` for terms like "SaaS" that are business models rather than formal industry categories.'),
|
|
@@ -31,11 +33,13 @@ export const searchCompaniesSchema = {
|
|
|
31
33
|
min_workforce_growth_pct: z.number().optional().describe('Minimum workforce growth % over the past 12 months (e.g. 10 for 10%)'),
|
|
32
34
|
linkedin_search_url: z.string().optional().describe('LinkedIn Sales Navigator company search URL — when provided, enables URL-based prospect discovery as a final fallback. Most users should leave this unset.'),
|
|
33
35
|
limit: z.number().min(1).max(500).default(25).describe('Max results to return (default: 25, max: 500). Pulls above 100 auto-paginate the underlying provider; on Apollo each 100 companies costs 1 credit, billed only for pages actually returned.'),
|
|
36
|
+
fields: z.enum(['compact', 'verbose']).default('compact').describe('Response shape. "compact" (default) returns a small per-company record: name, domain, website, linkedin_url, employees, revenue, industry, country, city, founded_year, categories — plus total. "verbose" returns the raw provider passthrough (funding-round history, technologies, keyword arrays, NAICS codes, all socials, descriptions) — only use when you specifically need those fields.'),
|
|
34
37
|
use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('search_companies').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export async function searchCompaniesHandler(input: Record<string, unknown>) {
|
|
38
|
-
const { use_providers: rawUseProviders, ...restInput } = input
|
|
41
|
+
const { use_providers: rawUseProviders, fields: rawFields, ...restInput } = input
|
|
42
|
+
const fields: 'compact' | 'verbose' = rawFields === 'verbose' ? 'verbose' : 'compact'
|
|
39
43
|
const resolved = resolvePreferredProviders('search_companies', restInput, rawUseProviders)
|
|
40
44
|
if (!resolved.ok) {
|
|
41
45
|
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
@@ -50,5 +54,8 @@ export async function searchCompaniesHandler(input: Record<string, unknown>) {
|
|
|
50
54
|
if (typeof totalEntries === 'number') {
|
|
51
55
|
;(result._meta as Record<string, unknown>).total_entries = totalEntries
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
const payload = fields === 'compact'
|
|
58
|
+
? { ...result, data: compactCompaniesPayload(result.data) }
|
|
59
|
+
: result
|
|
60
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(payload) }] }
|
|
54
61
|
}
|