@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.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BjC,CAAA;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;GAE1E"}
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":"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"}
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,20CAA20C;IAC30C,uXAAuX,CAAA;AAEzX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qSAAqS,CAAC;IACxV,kBAAkB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4XAA4X,CAAC;IACjc,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;IACpG,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uIAAuI,CAAC;IAC3L,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0QAA0Q,CAAC;IAC/T,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qHAAqH,CAAC;IAC5K,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IACvE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IACvE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IAC1E,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IACxE,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gGAAgG,CAAC;IACzJ,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;IACxF,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;IACxF,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;IACvF,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;IACrF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IACrF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IACrF,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;IACnG,kBAAkB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IACrG,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oDAAoD,CAAC;IAChH,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;IACxF,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6LAA6L,CAAC;IACvP,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4JAA4J,CAAC;IACjN,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,6LAA6L,CAAC;IACrP,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,mYAAmY,CAAC;IACvc,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CACtW,CAAA;AAED,8EAA8E;AAC9E,4EAA4E;AAC5E,gFAAgF;AAChF,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,+EAA+E;AAC/E,iEAAiE;AACjE,MAAM,qBAAqB,GAAG;IAC5B,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc;IAClE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB;IACxE,gBAAgB,EAAE,oBAAoB,EAAE,oBAAoB;IAC5D,kBAAkB,EAAE,kBAAkB,EAAE,aAAa,EAAE,aAAa;IACpE,WAAW,EAAE,0BAA0B,EAAE,qBAAqB;CACtD,CAAA;AAEV,iFAAiF;AACjF,gFAAgF;AAChF,oFAAoF;AACpF,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAA;IAC3E,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,KAAK,IAAI,CAAA;IACrD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAA;IACjH,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,uBAAuB,CAAC,KAA8B;IAC7D,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC1E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAA8B;IACzE,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,KAAK,EACH,0MAA0M;qBAC7M,CAAC;iBACH;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAA;AAC7C,CAAC"}
@@ -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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.3.27",
3
+ "version": "0.3.31",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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
- initClient()
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: '0.1.0',
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('ai-ark-companies is last (priority 18)', () => {
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('ai-ark-companies')
396
+ expect(ps[ps.length - 1].id).toBe('discolike')
397
+ expect(ps[ps.length - 2].id).toBe('ai-ark-companies')
397
398
  })
398
399
  })
@@ -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 17 providers in priority order', () => {
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
+ })