@coldiq/mcp 0.3.27 → 0.3.31
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/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +66 -0
- package/dist/registry.js.map +1 -1
- package/dist/tools/enrich-company-bulk.d.ts +2 -2
- package/dist/tools/enrich-person-bulk.d.ts +2 -2
- package/dist/tools/find-emails-bulk.d.ts +2 -2
- package/dist/tools/find-emails.d.ts +2 -2
- package/dist/tools/search-companies.d.ts +7 -0
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +49 -1
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +20 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +12 -3
- package/src/registry.ts +55 -0
- package/src/tools/search-companies.ts +48 -1
- package/src/version.ts +19 -0
- package/tests/registry-search-companies.test.ts +3 -2
- package/tests/registry.test.ts +4 -3
- package/tests/tools/search-companies.test.ts +94 -0
- package/tests/version.test.ts +16 -0
|
@@ -10,15 +10,15 @@ export declare const enrichPersonBulkSchema: {
|
|
|
10
10
|
company_name: z.ZodOptional<z.ZodString>;
|
|
11
11
|
domain: z.ZodOptional<z.ZodString>;
|
|
12
12
|
}, "strip", z.ZodTypeAny, {
|
|
13
|
-
linkedin_url?: string | undefined;
|
|
14
13
|
domain?: string | undefined;
|
|
14
|
+
linkedin_url?: string | undefined;
|
|
15
15
|
first_name?: string | undefined;
|
|
16
16
|
company_name?: string | undefined;
|
|
17
17
|
last_name?: string | undefined;
|
|
18
18
|
email?: string | undefined;
|
|
19
19
|
}, {
|
|
20
|
-
linkedin_url?: string | undefined;
|
|
21
20
|
domain?: string | undefined;
|
|
21
|
+
linkedin_url?: string | undefined;
|
|
22
22
|
first_name?: string | undefined;
|
|
23
23
|
company_name?: string | undefined;
|
|
24
24
|
last_name?: string | undefined;
|
|
@@ -9,15 +9,15 @@ export declare const findEmailsBulkSchema: {
|
|
|
9
9
|
domain: z.ZodOptional<z.ZodString>;
|
|
10
10
|
linkedin_url: z.ZodOptional<z.ZodString>;
|
|
11
11
|
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
domain?: string | undefined;
|
|
12
13
|
id?: string | undefined;
|
|
13
14
|
linkedin_url?: string | undefined;
|
|
14
|
-
domain?: string | undefined;
|
|
15
15
|
first_name?: string | undefined;
|
|
16
16
|
last_name?: string | undefined;
|
|
17
17
|
}, {
|
|
18
|
+
domain?: string | undefined;
|
|
18
19
|
id?: string | undefined;
|
|
19
20
|
linkedin_url?: string | undefined;
|
|
20
|
-
domain?: string | undefined;
|
|
21
21
|
first_name?: string | undefined;
|
|
22
22
|
last_name?: string | undefined;
|
|
23
23
|
}>, "many">;
|
|
@@ -10,14 +10,14 @@ export declare const findEmailsSchema: {
|
|
|
10
10
|
linkedin_url: z.ZodOptional<z.ZodString>;
|
|
11
11
|
}, "strip", z.ZodTypeAny, {
|
|
12
12
|
id: string;
|
|
13
|
-
linkedin_url?: string | undefined;
|
|
14
13
|
domain?: string | undefined;
|
|
14
|
+
linkedin_url?: string | undefined;
|
|
15
15
|
first_name?: string | undefined;
|
|
16
16
|
last_name?: string | undefined;
|
|
17
17
|
}, {
|
|
18
18
|
id: string;
|
|
19
|
-
linkedin_url?: string | undefined;
|
|
20
19
|
domain?: string | undefined;
|
|
20
|
+
linkedin_url?: string | undefined;
|
|
21
21
|
first_name?: string | undefined;
|
|
22
22
|
last_name?: string | undefined;
|
|
23
23
|
}>, "many">;
|
|
@@ -3,6 +3,7 @@ export declare const searchCompaniesName = "search_companies";
|
|
|
3
3
|
export declare const searchCompaniesDescription: string;
|
|
4
4
|
export declare const searchCompaniesSchema: {
|
|
5
5
|
keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
6
|
+
similar_to_domains: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
6
7
|
countries: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
8
|
locations: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
8
9
|
industries: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -34,5 +35,11 @@ export declare function searchCompaniesHandler(input: Record<string, unknown>):
|
|
|
34
35
|
text: string;
|
|
35
36
|
}];
|
|
36
37
|
isError?: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
content: {
|
|
40
|
+
type: "text";
|
|
41
|
+
text: string;
|
|
42
|
+
}[];
|
|
43
|
+
isError: boolean;
|
|
37
44
|
}>;
|
|
38
45
|
//# sourceMappingURL=search-companies.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-companies.d.ts","sourceRoot":"","sources":["../../src/tools/search-companies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,mBAAmB,qBAAqB,CAAA;AAErD,eAAO,MAAM,0BAA0B,QAEkV,CAAA;AAEzX,eAAO,MAAM,qBAAqB
|
|
1
|
+
{"version":3,"file":"search-companies.d.ts","sourceRoot":"","sources":["../../src/tools/search-companies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,mBAAmB,qBAAqB,CAAA;AAErD,eAAO,MAAM,0BAA0B,QAEkV,CAAA;AAEzX,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BjC,CAAA;AAkCD,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;GAgB1E"}
|
|
@@ -2,10 +2,11 @@ import { z } from 'zod';
|
|
|
2
2
|
import { callVerb } from '../verb-client.js';
|
|
3
3
|
import { getProvidersForCapability } from '../utils/provider-resolver.js';
|
|
4
4
|
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. For "fast-growing" companies there is no exact workforce-growth % filter anywhere — pass min_workforce_growth_pct (and/or is_hiring / a recent-funding filter) and ColdIQ routes to active-hiring and recently-funded providers, the practical proxies for growth. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
5
|
+
export const searchCompaniesDescription = 'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. To build a lookalike TAM from example accounts, pass `similar_to_domains` (seed company domains) — the request routes to DiscoLike\'s lookalike discovery over 70M companies, optionally narrowed by keywords/geo/size. Routing is otherwise 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. For "fast-growing" companies there is no exact workforce-growth % filter anywhere — pass min_workforce_growth_pct (and/or is_hiring / a recent-funding filter) and ColdIQ routes to active-hiring and recently-funded providers, the practical proxies for growth. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
6
6
|
'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).';
|
|
7
7
|
export const searchCompaniesSchema = {
|
|
8
8
|
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.'),
|
|
9
|
+
similar_to_domains: z.array(z.string()).max(10).optional().describe('Seed company domains to find lookalikes of (up to 10) — e.g. ["stripe.com", "lemlist.com"]. Routes to DiscoLike\'s lookalike discovery over 70M companies; the best way to build a net-new TAM from example accounts. Optionally narrow with keywords / industries / countries / employee range. Not compatible with tech-stack, funding, revenue, founded-year, or hiring/growth filters.'),
|
|
9
10
|
countries: z.array(z.string()).optional().describe('2-letter ISO country codes (e.g. ["FR", "US"])'),
|
|
10
11
|
locations: z.array(z.string()).optional().describe('City/region filters as free-text strings (e.g. ["Paris, France", "San Francisco, California"]). Narrows results beyond country-level.'),
|
|
11
12
|
industries: z.array(z.string()).optional().describe('Formal industry-taxonomy categories — e.g. ["Financial Services", "Hospitals and Health Care"]. Use only for strict industry classification; for business models or themes like "SaaS" or "fintech", use `keywords` instead. Both fields are searched together when set.'),
|
|
@@ -31,7 +32,54 @@ export const searchCompaniesSchema = {
|
|
|
31
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.'),
|
|
32
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.`),
|
|
33
34
|
};
|
|
35
|
+
// A call must carry at least one filter that actually NARROWS the result set,
|
|
36
|
+
// otherwise it forwards an unbounded query upstream and bills for arbitrary
|
|
37
|
+
// companies. This is an explicit allowlist of narrowing keys, not a denylist of
|
|
38
|
+
// response-shaping ones, on purpose: a future response-shaping field (e.g. a
|
|
39
|
+
// `sort` or `page` knob) added to the schema must never silently count as a
|
|
40
|
+
// filter and reopen the billing hole. Exclusion fields (`exclude_*`) are
|
|
41
|
+
// deliberately absent — excluding a domain narrows nothing on its own. Kept in
|
|
42
|
+
// sync with the server-side guard in src/router/verb-handler.ts.
|
|
43
|
+
const NARROWING_FILTER_KEYS = [
|
|
44
|
+
'keywords', 'countries', 'locations', 'industries', 'technologies',
|
|
45
|
+
'min_employees', 'max_employees', 'min_founded_year', 'max_founded_year',
|
|
46
|
+
'funding_stages', 'min_funding_amount', 'max_funding_amount',
|
|
47
|
+
'min_funding_year', 'max_funding_year', 'min_revenue', 'max_revenue',
|
|
48
|
+
'is_hiring', 'min_workforce_growth_pct', 'linkedin_search_url',
|
|
49
|
+
];
|
|
50
|
+
// A value narrows only when it actually constrains: a non-blank string, an array
|
|
51
|
+
// with at least one non-blank element, a non-zero finite number (a `min_*` of 0
|
|
52
|
+
// is a no-op bound), or boolean `true` (is_hiring is ignored upstream unless true).
|
|
53
|
+
function isNarrowingValue(value) {
|
|
54
|
+
if (value === undefined || value === null)
|
|
55
|
+
return false;
|
|
56
|
+
if (typeof value === 'string')
|
|
57
|
+
return value.trim().length > 0;
|
|
58
|
+
if (typeof value === 'number')
|
|
59
|
+
return Number.isFinite(value) && value !== 0;
|
|
60
|
+
if (typeof value === 'boolean')
|
|
61
|
+
return value === true;
|
|
62
|
+
if (Array.isArray(value))
|
|
63
|
+
return value.some((el) => (typeof el === 'string' ? el.trim().length > 0 : el != null));
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function hasDiscriminatingFilter(input) {
|
|
67
|
+
return NARROWING_FILTER_KEYS.some((key) => isNarrowingValue(input[key]));
|
|
68
|
+
}
|
|
34
69
|
export async function searchCompaniesHandler(input) {
|
|
70
|
+
if (!hasDiscriminatingFilter(input)) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
error: 'search_companies needs at least one filter (keywords, countries, industries, employee range, technologies, funding, …). An unfiltered search would return arbitrary companies and still consume credits.',
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
35
83
|
return callVerb('/companies/search', input);
|
|
36
84
|
}
|
|
37
85
|
//# 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,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAEzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AAErD,MAAM,CAAC,MAAM,0BAA0B,GACrC,
|
|
1
|
+
{"version":3,"file":"search-companies.js","sourceRoot":"","sources":["../../src/tools/search-companies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP server reports its own npm version to the client at init. package.json
|
|
3
|
+
* sits outside tsconfig rootDir, so it can't be imported — read it at runtime.
|
|
4
|
+
* The built entry (dist/index.js) sits exactly one directory below the package
|
|
5
|
+
* root, so '../package.json' resolves in both `tsx src/…` dev and the published
|
|
6
|
+
* dist layout. Falls back to '0.0.0' rather than throwing, so a packaging change
|
|
7
|
+
* that moves the file can never crash server startup over a cosmetic version string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readPackageVersion(fromUrl: string): string;
|
|
10
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ1D"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
/**
|
|
3
|
+
* The MCP server reports its own npm version to the client at init. package.json
|
|
4
|
+
* sits outside tsconfig rootDir, so it can't be imported — read it at runtime.
|
|
5
|
+
* The built entry (dist/index.js) sits exactly one directory below the package
|
|
6
|
+
* root, so '../package.json' resolves in both `tsx src/…` dev and the published
|
|
7
|
+
* dist layout. Falls back to '0.0.0' rather than throwing, so a packaging change
|
|
8
|
+
* that moves the file can never crash server startup over a cosmetic version string.
|
|
9
|
+
*/
|
|
10
|
+
export function readPackageVersion(fromUrl) {
|
|
11
|
+
try {
|
|
12
|
+
const require = createRequire(fromUrl);
|
|
13
|
+
const { version } = require('../package.json');
|
|
14
|
+
return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return '0.0.0';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAA;QACtC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAyB,CAAA;QACtE,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAA;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAA;IAChB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
5
5
|
import { initClient } from './client.js'
|
|
6
|
+
import { readPackageVersion } from './version.js'
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
searchCompaniesName,
|
|
@@ -182,13 +183,21 @@ import {
|
|
|
182
183
|
loadSkillHandler,
|
|
183
184
|
} from './tools/load-skill.js'
|
|
184
185
|
|
|
185
|
-
// Initialize HTTP client (reads env vars)
|
|
186
|
-
|
|
186
|
+
// Initialize HTTP client (reads env vars). A missing/empty COLDIQ_API_KEY is a
|
|
187
|
+
// user-config error, not a bug — print the message without a stack trace.
|
|
188
|
+
try {
|
|
189
|
+
initClient()
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const packageVersion = readPackageVersion(import.meta.url)
|
|
187
196
|
|
|
188
197
|
const server = new McpServer(
|
|
189
198
|
{
|
|
190
199
|
name: 'coldiq',
|
|
191
|
-
version:
|
|
200
|
+
version: packageVersion,
|
|
192
201
|
},
|
|
193
202
|
{
|
|
194
203
|
// Surfaced to the model at init. Enforces batching on the multi-company
|
package/src/registry.ts
CHANGED
|
@@ -1061,6 +1061,48 @@ const searchCompaniesProviders: ProviderEntry[] = [
|
|
|
1061
1061
|
return isNonEmptyArray(d.content)
|
|
1062
1062
|
},
|
|
1063
1063
|
},
|
|
1064
|
+
{
|
|
1065
|
+
// Tail fallback — semantic/ICP-text discovery (11 credits/query + 0.21/result).
|
|
1066
|
+
id: 'discolike',
|
|
1067
|
+
endpoint: '/discolike/discover',
|
|
1068
|
+
method: 'GET',
|
|
1069
|
+
priority: 19,
|
|
1070
|
+
isApplicable: (input) => {
|
|
1071
|
+
if (!isNonEmptyArray(input.similar_to_domains) && !isNonEmptyArray(input.keywords) && !isNonEmptyArray(input.industries)) return false
|
|
1072
|
+
if (isNonEmptyArray(input.locations)) return false
|
|
1073
|
+
if (input.linkedin_search_url) return false
|
|
1074
|
+
if (isNonEmptyArray(input.technologies)) return false
|
|
1075
|
+
if (isNonEmptyArray(input.funding_stages)) return false
|
|
1076
|
+
if (input.min_funding_amount !== undefined || input.max_funding_amount !== undefined) return false
|
|
1077
|
+
if (input.min_funding_year !== undefined || input.max_funding_year !== undefined) return false
|
|
1078
|
+
if (input.min_revenue !== undefined || input.max_revenue !== undefined) return false
|
|
1079
|
+
if (input.min_founded_year !== undefined || input.max_founded_year !== undefined) return false
|
|
1080
|
+
if (isNonEmptyArray(input.exclude_industries) || isNonEmptyArray(input.exclude_countries)) return false
|
|
1081
|
+
if (typeof input.min_workforce_growth_pct === 'number' || input.is_hiring === true) return false
|
|
1082
|
+
return true
|
|
1083
|
+
},
|
|
1084
|
+
mapParams: (input) => {
|
|
1085
|
+
const icpParts = [
|
|
1086
|
+
...((input.keywords as string[] | undefined) ?? []),
|
|
1087
|
+
...((input.industries as string[] | undefined) ?? []),
|
|
1088
|
+
]
|
|
1089
|
+
const limit = (input.limit as number | undefined) ?? 25
|
|
1090
|
+
return {
|
|
1091
|
+
queryParams: {
|
|
1092
|
+
// Seed domains drive lookalike discovery; icp_text narrows semantically.
|
|
1093
|
+
...(isNonEmptyArray(input.similar_to_domains) ? { domain: (input.similar_to_domains as string[]).join(',') } : {}),
|
|
1094
|
+
...(icpParts.length > 0 ? { icp_text: icpParts.join(' ') } : {}),
|
|
1095
|
+
...(isNonEmptyArray(input.countries) ? { country: (input.countries as string[]).join(',') } : {}),
|
|
1096
|
+
...(input.min_employees !== undefined || input.max_employees !== undefined
|
|
1097
|
+
? { employee_range: `${(input.min_employees as number) ?? 1},${(input.max_employees as number) ?? 1000000}` }
|
|
1098
|
+
: {}),
|
|
1099
|
+
...(isNonEmptyArray(input.exclude_domains) ? { exclude_domain: (input.exclude_domains as string[]).join(',') } : {}),
|
|
1100
|
+
max_records: String(Math.min(Math.max(limit, 5), 1000)),
|
|
1101
|
+
},
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
hasResult: (data) => Array.isArray(data) && data.length > 0,
|
|
1105
|
+
},
|
|
1064
1106
|
]
|
|
1065
1107
|
|
|
1066
1108
|
// ---------------------------------------------------------------------------
|
|
@@ -2074,6 +2116,19 @@ const enrichCompanyProviders: ProviderEntry[] = [
|
|
|
2074
2116
|
mapParams: (input) => ({ body: { company_url: input.linkedin_url } }),
|
|
2075
2117
|
hasResult: (data) => hasAnyKey(data, ['name', 'domain', 'website', 'company']),
|
|
2076
2118
|
},
|
|
2119
|
+
{
|
|
2120
|
+
// Last-resort fallback (12 credits flat): SSL-derived firmographics.
|
|
2121
|
+
id: 'discolike',
|
|
2122
|
+
endpoint: '/discolike/bizdata',
|
|
2123
|
+
method: 'GET',
|
|
2124
|
+
priority: 15,
|
|
2125
|
+
isApplicable: (input) => !!input.domain,
|
|
2126
|
+
mapParams: (input) => ({ queryParams: { domain: input.domain as string } }),
|
|
2127
|
+
hasResult: (data) => {
|
|
2128
|
+
const d = data as Record<string, unknown>
|
|
2129
|
+
return typeof d?.name === 'string' && d.name.length > 0
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2077
2132
|
]
|
|
2078
2133
|
|
|
2079
2134
|
// ---------------------------------------------------------------------------
|
|
@@ -5,11 +5,12 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
|
5
5
|
export const searchCompaniesName = 'search_companies'
|
|
6
6
|
|
|
7
7
|
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. For "fast-growing" companies there is no exact workforce-growth % filter anywhere — pass min_workforce_growth_pct (and/or is_hiring / a recent-funding filter) and ColdIQ routes to active-hiring and recently-funded providers, the practical proxies for growth. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
8
|
+
'Search B2B companies by industry, size, geography, founding year, tech stack, funding, revenue, exclusion lists, and hiring signals. To build a lookalike TAM from example accounts, pass `similar_to_domains` (seed company domains) — the request routes to DiscoLike\'s lookalike discovery over 70M companies, optionally narrowed by keywords/geo/size. Routing is otherwise 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. For "fast-growing" companies there is no exact workforce-growth % filter anywhere — pass min_workforce_growth_pct (and/or is_hiring / a recent-funding filter) and ColdIQ routes to active-hiring and recently-funded providers, the practical proxies for growth. ColdIQ automatically picks the best provider — pass use_providers only if you need a specific tool. Default limit: 25. ' +
|
|
9
9
|
'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).'
|
|
10
10
|
|
|
11
11
|
export const searchCompaniesSchema = {
|
|
12
12
|
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.'),
|
|
13
|
+
similar_to_domains: z.array(z.string()).max(10).optional().describe('Seed company domains to find lookalikes of (up to 10) — e.g. ["stripe.com", "lemlist.com"]. Routes to DiscoLike\'s lookalike discovery over 70M companies; the best way to build a net-new TAM from example accounts. Optionally narrow with keywords / industries / countries / employee range. Not compatible with tech-stack, funding, revenue, founded-year, or hiring/growth filters.'),
|
|
13
14
|
countries: z.array(z.string()).optional().describe('2-letter ISO country codes (e.g. ["FR", "US"])'),
|
|
14
15
|
locations: z.array(z.string()).optional().describe('City/region filters as free-text strings (e.g. ["Paris, France", "San Francisco, California"]). Narrows results beyond country-level.'),
|
|
15
16
|
industries: z.array(z.string()).optional().describe('Formal industry-taxonomy categories — e.g. ["Financial Services", "Hospitals and Health Care"]. Use only for strict industry classification; for business models or themes like "SaaS" or "fintech", use `keywords` instead. Both fields are searched together when set.'),
|
|
@@ -36,6 +37,52 @@ export const searchCompaniesSchema = {
|
|
|
36
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.`),
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// A call must carry at least one filter that actually NARROWS the result set,
|
|
41
|
+
// otherwise it forwards an unbounded query upstream and bills for arbitrary
|
|
42
|
+
// companies. This is an explicit allowlist of narrowing keys, not a denylist of
|
|
43
|
+
// response-shaping ones, on purpose: a future response-shaping field (e.g. a
|
|
44
|
+
// `sort` or `page` knob) added to the schema must never silently count as a
|
|
45
|
+
// filter and reopen the billing hole. Exclusion fields (`exclude_*`) are
|
|
46
|
+
// deliberately absent — excluding a domain narrows nothing on its own. Kept in
|
|
47
|
+
// sync with the server-side guard in src/router/verb-handler.ts.
|
|
48
|
+
const NARROWING_FILTER_KEYS = [
|
|
49
|
+
'keywords', 'countries', 'locations', 'industries', 'technologies',
|
|
50
|
+
'min_employees', 'max_employees', 'min_founded_year', 'max_founded_year',
|
|
51
|
+
'funding_stages', 'min_funding_amount', 'max_funding_amount',
|
|
52
|
+
'min_funding_year', 'max_funding_year', 'min_revenue', 'max_revenue',
|
|
53
|
+
'is_hiring', 'min_workforce_growth_pct', 'linkedin_search_url',
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
// A value narrows only when it actually constrains: a non-blank string, an array
|
|
57
|
+
// with at least one non-blank element, a non-zero finite number (a `min_*` of 0
|
|
58
|
+
// is a no-op bound), or boolean `true` (is_hiring is ignored upstream unless true).
|
|
59
|
+
function isNarrowingValue(value: unknown): boolean {
|
|
60
|
+
if (value === undefined || value === null) return false
|
|
61
|
+
if (typeof value === 'string') return value.trim().length > 0
|
|
62
|
+
if (typeof value === 'number') return Number.isFinite(value) && value !== 0
|
|
63
|
+
if (typeof value === 'boolean') return value === true
|
|
64
|
+
if (Array.isArray(value)) return value.some((el) => (typeof el === 'string' ? el.trim().length > 0 : el != null))
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hasDiscriminatingFilter(input: Record<string, unknown>): boolean {
|
|
69
|
+
return NARROWING_FILTER_KEYS.some((key) => isNarrowingValue(input[key]))
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
export async function searchCompaniesHandler(input: Record<string, unknown>) {
|
|
73
|
+
if (!hasDiscriminatingFilter(input)) {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: 'text' as const,
|
|
78
|
+
text: JSON.stringify({
|
|
79
|
+
error:
|
|
80
|
+
'search_companies needs at least one filter (keywords, countries, industries, employee range, technologies, funding, …). An unfiltered search would return arbitrary companies and still consume credits.',
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
isError: true,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
40
87
|
return callVerb('/companies/search', input)
|
|
41
88
|
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The MCP server reports its own npm version to the client at init. package.json
|
|
5
|
+
* sits outside tsconfig rootDir, so it can't be imported — read it at runtime.
|
|
6
|
+
* The built entry (dist/index.js) sits exactly one directory below the package
|
|
7
|
+
* root, so '../package.json' resolves in both `tsx src/…` dev and the published
|
|
8
|
+
* dist layout. Falls back to '0.0.0' rather than throwing, so a packaging change
|
|
9
|
+
* that moves the file can never crash server startup over a cosmetic version string.
|
|
10
|
+
*/
|
|
11
|
+
export function readPackageVersion(fromUrl: string): string {
|
|
12
|
+
try {
|
|
13
|
+
const require = createRequire(fromUrl)
|
|
14
|
+
const { version } = require('../package.json') as { version?: string }
|
|
15
|
+
return typeof version === 'string' && version.length > 0 ? version : '0.0.0'
|
|
16
|
+
} catch {
|
|
17
|
+
return '0.0.0'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -391,8 +391,9 @@ describe('search_companies priority ordering', () => {
|
|
|
391
391
|
}
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
-
it('
|
|
394
|
+
it('discolike is last (priority 19), after ai-ark-companies (18)', () => {
|
|
395
395
|
const ps = providers()
|
|
396
|
-
expect(ps[ps.length - 1].id).toBe('
|
|
396
|
+
expect(ps[ps.length - 1].id).toBe('discolike')
|
|
397
|
+
expect(ps[ps.length - 2].id).toBe('ai-ark-companies')
|
|
397
398
|
})
|
|
398
399
|
})
|
package/tests/registry.test.ts
CHANGED
|
@@ -459,7 +459,7 @@ describe('registry', () => {
|
|
|
459
459
|
expect(ld.isApplicable!({ linkedin_search_url: 'https://...' })).toBe(true)
|
|
460
460
|
})
|
|
461
461
|
|
|
462
|
-
it('search_companies has
|
|
462
|
+
it('search_companies has 18 providers in priority order', () => {
|
|
463
463
|
const providers = getProviders('search_companies')
|
|
464
464
|
expect(providers.map((p) => p.id)).toEqual([
|
|
465
465
|
'companyenrich',
|
|
@@ -479,6 +479,7 @@ describe('registry', () => {
|
|
|
479
479
|
'linkupapi-hiring',
|
|
480
480
|
'prospeo-search-company',
|
|
481
481
|
'ai-ark-companies',
|
|
482
|
+
'discolike',
|
|
482
483
|
])
|
|
483
484
|
})
|
|
484
485
|
|
|
@@ -833,12 +834,12 @@ describe('registry', () => {
|
|
|
833
834
|
})
|
|
834
835
|
|
|
835
836
|
describe('enrich_company', () => {
|
|
836
|
-
it('providers are ordered companyenrich → pdl → apollo → limadata → prospeo → companyenrich_props → blitzapi → wiza → findymail → icypeas → builtwith → openmart → linkupapi-by-domain → linkupapi-by-url', () => {
|
|
837
|
+
it('providers are ordered companyenrich → pdl → apollo → limadata → prospeo → companyenrich_props → blitzapi → wiza → findymail → icypeas → builtwith → openmart → linkupapi-by-domain → linkupapi-by-url → discolike', () => {
|
|
837
838
|
const ids = getProviders('enrich_company').map((p) => p.id)
|
|
838
839
|
expect(ids).toEqual([
|
|
839
840
|
'companyenrich', 'pdl', 'apollo', 'limadata', 'prospeo',
|
|
840
841
|
'companyenrich_props', 'blitzapi', 'wiza', 'findymail', 'icypeas',
|
|
841
|
-
'builtwith', 'openmart', 'linkupapi-by-domain', 'linkupapi-by-url',
|
|
842
|
+
'builtwith', 'openmart', 'linkupapi-by-domain', 'linkupapi-by-url', 'discolike',
|
|
842
843
|
])
|
|
843
844
|
})
|
|
844
845
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { searchCompaniesHandler } from '../../src/tools/search-companies.js'
|
|
4
|
+
|
|
5
|
+
describe('search_companies handler', () => {
|
|
6
|
+
const originalFetch = globalThis.fetch
|
|
7
|
+
let fetchSpy: ReturnType<typeof vi.fn>
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
initClient('http://test-api.local', 'test-key')
|
|
11
|
+
// Default: a spy that records calls but must NOT be invoked by rejection paths.
|
|
12
|
+
// Forwarding tests override globalThis.fetch with a real Response mock.
|
|
13
|
+
fetchSpy = vi.fn()
|
|
14
|
+
globalThis.fetch = fetchSpy as typeof fetch
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
globalThis.fetch = originalFetch
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const okFetch = () =>
|
|
22
|
+
vi.fn(async () => new Response(JSON.stringify({ companies: [], total: 0 }), { status: 200 })) as typeof fetch
|
|
23
|
+
|
|
24
|
+
it('rejects a call with no discriminating filter without hitting the API', async () => {
|
|
25
|
+
const result = await searchCompaniesHandler({})
|
|
26
|
+
|
|
27
|
+
expect(result.isError).toBe(true)
|
|
28
|
+
expect(JSON.parse(result.content[0].text).error).toMatch(/at least one filter/)
|
|
29
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects when only response-shaping fields are set', async () => {
|
|
33
|
+
const result = await searchCompaniesHandler({ limit: 200, fields: 'compact', use_providers: ['apollo'] })
|
|
34
|
+
|
|
35
|
+
expect(result.isError).toBe(true)
|
|
36
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('rejects when filters are present but empty', async () => {
|
|
40
|
+
const result = await searchCompaniesHandler({ keywords: [], countries: [], linkedin_search_url: ' ' })
|
|
41
|
+
|
|
42
|
+
expect(result.isError).toBe(true)
|
|
43
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('rejects arrays that contain only blank strings', async () => {
|
|
47
|
+
const result = await searchCompaniesHandler({ keywords: ['', ' '] })
|
|
48
|
+
|
|
49
|
+
expect(result.isError).toBe(true)
|
|
50
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('rejects is_hiring:false (upstream ignores it unless true)', async () => {
|
|
54
|
+
const result = await searchCompaniesHandler({ is_hiring: false, limit: 200 })
|
|
55
|
+
|
|
56
|
+
expect(result.isError).toBe(true)
|
|
57
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('rejects no-op zero min-bounds', async () => {
|
|
61
|
+
const result = await searchCompaniesHandler({ min_employees: 0, min_funding_amount: 0 })
|
|
62
|
+
|
|
63
|
+
expect(result.isError).toBe(true)
|
|
64
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects an exclusion-only search (excludes narrow nothing on their own)', async () => {
|
|
68
|
+
const result = await searchCompaniesHandler({ exclude_domains: ['coldiq.com'] })
|
|
69
|
+
|
|
70
|
+
expect(result.isError).toBe(true)
|
|
71
|
+
expect(fetchSpy).not.toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('forwards to the verb when at least one real filter is set', async () => {
|
|
75
|
+
globalThis.fetch = vi.fn(async () => {
|
|
76
|
+
return new Response(JSON.stringify({ companies: [], total: 0, _meta: { provider: 'apollo' } }), { status: 200 })
|
|
77
|
+
}) as typeof fetch
|
|
78
|
+
|
|
79
|
+
const result = await searchCompaniesHandler({ keywords: ['sales automation'], limit: 5 })
|
|
80
|
+
|
|
81
|
+
expect(result.isError).toBeFalsy()
|
|
82
|
+
expect(globalThis.fetch).toHaveBeenCalledOnce()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('treats boolean true and non-zero numeric filters as discriminating', async () => {
|
|
86
|
+
globalThis.fetch = okFetch()
|
|
87
|
+
|
|
88
|
+
const hiring = await searchCompaniesHandler({ is_hiring: true })
|
|
89
|
+
const sized = await searchCompaniesHandler({ min_employees: 11 })
|
|
90
|
+
|
|
91
|
+
expect(hiring.isError).toBeFalsy()
|
|
92
|
+
expect(sized.isError).toBeFalsy()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readPackageVersion } from '../src/version.js'
|
|
3
|
+
|
|
4
|
+
describe('readPackageVersion', () => {
|
|
5
|
+
it('reports the real package.json version relative to the caller URL', () => {
|
|
6
|
+
// This test file sits at mcp/tests/, so '../package.json' from here is mcp/package.json.
|
|
7
|
+
const version = readPackageVersion(import.meta.url)
|
|
8
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+/)
|
|
9
|
+
expect(version).not.toBe('0.1.0') // the old hardcoded placeholder
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('falls back to 0.0.0 instead of throwing when package.json cannot be resolved', () => {
|
|
13
|
+
const version = readPackageVersion('file:///nonexistent/deeply/nested/module.js')
|
|
14
|
+
expect(version).toBe('0.0.0')
|
|
15
|
+
})
|
|
16
|
+
})
|