@coldiq/mcp 0.4.2 → 0.4.4

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.
Files changed (76) hide show
  1. package/dist/index.js +2 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/tools/cancel-bulk-job.d.ts +14 -0
  4. package/dist/tools/cancel-bulk-job.d.ts.map +1 -0
  5. package/dist/tools/cancel-bulk-job.js +18 -0
  6. package/dist/tools/cancel-bulk-job.js.map +1 -0
  7. package/dist/tools/enrich-company-bulk.d.ts.map +1 -1
  8. package/dist/tools/enrich-company-bulk.js +2 -1
  9. package/dist/tools/enrich-company-bulk.js.map +1 -1
  10. package/dist/tools/enrich-person-bulk.d.ts.map +1 -1
  11. package/dist/tools/enrich-person-bulk.js +2 -1
  12. package/dist/tools/enrich-person-bulk.js.map +1 -1
  13. package/dist/tools/extract-post-engagement.d.ts.map +1 -1
  14. package/dist/tools/extract-post-engagement.js +2 -2
  15. package/dist/tools/extract-post-engagement.js.map +1 -1
  16. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  17. package/dist/tools/fetch-page-content.js +1 -1
  18. package/dist/tools/fetch-page-content.js.map +1 -1
  19. package/dist/tools/find-email.js +1 -1
  20. package/dist/tools/find-email.js.map +1 -1
  21. package/dist/tools/find-emails-bulk.d.ts.map +1 -1
  22. package/dist/tools/find-emails-bulk.js +3 -2
  23. package/dist/tools/find-emails-bulk.js.map +1 -1
  24. package/dist/tools/find-emails.js +1 -1
  25. package/dist/tools/find-emails.js.map +1 -1
  26. package/dist/tools/find-influencers.js +2 -2
  27. package/dist/tools/find-influencers.js.map +1 -1
  28. package/dist/tools/find-people.d.ts.map +1 -1
  29. package/dist/tools/find-people.js +2 -2
  30. package/dist/tools/find-people.js.map +1 -1
  31. package/dist/tools/find-phone-bulk.d.ts.map +1 -1
  32. package/dist/tools/find-phone-bulk.js +3 -2
  33. package/dist/tools/find-phone-bulk.js.map +1 -1
  34. package/dist/tools/find-phone.d.ts +1 -1
  35. package/dist/tools/find-phone.d.ts.map +1 -1
  36. package/dist/tools/find-phone.js +2 -2
  37. package/dist/tools/find-phone.js.map +1 -1
  38. package/dist/tools/get-bulk-job.d.ts.map +1 -1
  39. package/dist/tools/get-bulk-job.js +3 -2
  40. package/dist/tools/get-bulk-job.js.map +1 -1
  41. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  42. package/dist/tools/get-place-reviews.js +2 -2
  43. package/dist/tools/get-place-reviews.js.map +1 -1
  44. package/dist/tools/search-ads.d.ts +1 -1
  45. package/dist/tools/search-ads.d.ts.map +1 -1
  46. package/dist/tools/search-ads.js +2 -2
  47. package/dist/tools/search-ads.js.map +1 -1
  48. package/dist/tools/search-places.d.ts +1 -1
  49. package/dist/tools/search-places.js +1 -1
  50. package/dist/tools/search-reddit.d.ts +1 -1
  51. package/dist/tools/search-reddit.d.ts.map +1 -1
  52. package/dist/tools/search-reddit.js +2 -2
  53. package/dist/tools/search-reddit.js.map +1 -1
  54. package/dist/tools/verify-emails-bulk.js +1 -1
  55. package/dist/tools/verify-emails-bulk.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/index.ts +2 -0
  58. package/src/tools/cancel-bulk-job.ts +22 -0
  59. package/src/tools/enrich-company-bulk.ts +2 -1
  60. package/src/tools/enrich-person-bulk.ts +2 -1
  61. package/src/tools/extract-post-engagement.ts +2 -2
  62. package/src/tools/fetch-page-content.ts +1 -1
  63. package/src/tools/find-email.ts +1 -1
  64. package/src/tools/find-emails-bulk.ts +3 -2
  65. package/src/tools/find-emails.ts +1 -1
  66. package/src/tools/find-influencers.ts +2 -2
  67. package/src/tools/find-people.ts +2 -2
  68. package/src/tools/find-phone-bulk.ts +3 -2
  69. package/src/tools/find-phone.ts +2 -2
  70. package/src/tools/get-bulk-job.ts +3 -2
  71. package/src/tools/get-place-reviews.ts +2 -2
  72. package/src/tools/search-ads.ts +2 -2
  73. package/src/tools/search-places.ts +1 -1
  74. package/src/tools/search-reddit.ts +2 -2
  75. package/src/tools/verify-emails-bulk.ts +1 -1
  76. package/tests/bulk-tools.test.ts +17 -0
@@ -2,14 +2,14 @@ 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 searchAdsName = 'search_ads';
5
- export const searchAdsDescription = 'Search live ad creatives across 4 ad libraries (Google Ads Transparency, LinkedIn Ad Library, Meta Ads Library, Twitter/X Ads) — a high-signal GTM input for competitive intelligence, ICP refinement, and pitch personalization. Routes by input: domains/advertiser_ids → Google only; search_urls → LinkedIn only; bare query → Google → Meta → Twitter waterfall. Use platform="google"|"linkedin"|"meta"|"twitter" to pin to one platform. All providers are async (~10–60s). Cost: ~5 credits per call (Twitter charges 1 credit per ad returned). Credits are fully refunded when a run returns zero ads. NOTE: Google Ads creatives return image URLs + creative IDs, not ad copy text — open the image URLs to read the ad. There is no "currently running only" filter; results can span past campaigns.';
5
+ export const searchAdsDescription = 'Search live ad creatives across 4 ad libraries (Google Ads Transparency, LinkedIn Ad Library, Meta Ads Library, Twitter/X Ads) — a high-signal GTM input for competitive intelligence, ICP refinement, and pitch personalization. Routes by input: domains/advertiser_ids → Google only; search_urls → LinkedIn only; bare query → Google → Meta → Twitter waterfall. Use platform="google"|"linkedin"|"meta"|"twitter" to pin to one platform. All providers are async (~10–60s). Pricing is returned in the response credit headers; credits are fully refunded when a run returns zero ads. NOTE: Google Ads creatives return image URLs + creative IDs, not ad copy text — open the image URLs to read the ad. There is no "currently running only" filter; results can span past campaigns.';
6
6
  export const searchAdsSchema = {
7
7
  query: z.string().optional().describe('Advertiser/company name or keyword. Routes to Google→Meta→Twitter when no platform-specific input is set.'),
8
8
  domains: z.array(z.string()).optional().describe('Company domains (e.g. ["salesforce.com"]). Routes to Google Ads only.'),
9
9
  advertiser_ids: z.array(z.string()).optional().describe('Google Ads Transparency advertiser IDs (e.g. ["AR16735076323512287233"]). Routes to Google Ads only.'),
10
10
  search_urls: z.array(z.string()).optional().describe('Pre-built LinkedIn Ad Library URLs from linkedin.com/ad-library. Routes to LinkedIn only. Build at linkedin.com/ad-library/search using accountOwner/countries/dateOption filters.'),
11
11
  country: z.string().optional().describe('ISO country code (2-letter for Meta/Twitter; longer accepted by Google as region).'),
12
- max_results: z.number().int().min(1).max(200).default(25).describe('Max ads per provider call (1–200). Per-provider caps: Google 1000, LinkedIn 200, Meta 200, Twitter 100. Twitter charges 1 credit per ad returned.'),
12
+ max_results: z.number().int().min(1).max(200).default(25).describe('Max ads per provider call (1–200). Per-provider caps: Google 1000, LinkedIn 200, Meta 200, Twitter 100. Pricing is returned in the response credit headers.'),
13
13
  ad_type: z.enum(['ALL', 'POLITICAL_AND_ISSUE_ADS']).optional().describe('Meta only. Defaults to ALL.'),
14
14
  start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Twitter only. Date range start, YYYY-MM-DD.'),
15
15
  end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Twitter only. Date range end, YYYY-MM-DD.'),
@@ -1 +1 @@
1
- {"version":3,"file":"search-ads.js","sourceRoot":"","sources":["../../src/tools/search-ads.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,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAC/B,oxBAAoxB,CAAA;AAEtxB,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2GAA2G,CAAC;IAElJ,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uEAAuE,CAAC;IACzH,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sGAAsG,CAAC;IAE/J,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oLAAoL,CAAC;IAE1O,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oFAAoF,CAAC;IAC7H,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,mJAAmJ,CAAC;IAEvN,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,yBAAyB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;IAEtG,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6CAA6C,CAAC;IACtH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;IAElH,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iGAAiG,CAAC;IAClL,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CAChW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAA8B;IACnE,OAAO,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;AACvC,CAAC"}
1
+ {"version":3,"file":"search-ads.js","sourceRoot":"","sources":["../../src/tools/search-ads.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,aAAa,GAAG,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kwBAAkwB,CAAA;AAEpwB,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2GAA2G,CAAC;IAElJ,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uEAAuE,CAAC;IACzH,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sGAAsG,CAAC;IAE/J,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oLAAoL,CAAC;IAE1O,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oFAAoF,CAAC;IAC7H,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,6JAA6J,CAAC;IAEjO,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,yBAAyB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;IAEtG,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6CAA6C,CAAC;IACtH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;IAElH,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iGAAiG,CAAC;IAClL,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CAChW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAA8B;IACnE,OAAO,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;AACvC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export declare const searchPlacesName = "search_places";
3
- export declare const searchPlacesDescription = "Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) \u2014 useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} \u2192 Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30\u2013120s) as fallback or for global coverage. Use provider=\"openmart\"|\"google_maps\" to pin to one. Cost: 1 credit per place returned (both providers). Results do NOT include review text \u2014 to fetch a place's reviews, pass its `url` to get_place_reviews.";
3
+ export declare const searchPlacesDescription = "Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) \u2014 useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} \u2192 Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30\u2013120s) as fallback or for global coverage. Use provider=\"openmart\"|\"google_maps\" to pin to one. Pricing is returned in the response credit headers. Results do NOT include review text \u2014 to fetch a place's reviews, pass its `url` to get_place_reviews.";
4
4
  export declare const searchPlacesSchema: {
5
5
  query: z.ZodOptional<z.ZodString>;
6
6
  country: z.ZodOptional<z.ZodString>;
@@ -2,7 +2,7 @@ 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 searchPlacesName = 'search_places';
5
- export const searchPlacesDescription = 'Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) — useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} → Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30–120s) as fallback or for global coverage. Use provider="openmart"|"google_maps" to pin to one. Cost: 1 credit per place returned (both providers). Results do NOT include review text — to fetch a place\'s reviews, pass its `url` to get_place_reviews.';
5
+ export const searchPlacesDescription = 'Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) — useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} → Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30–120s) as fallback or for global coverage. Use provider="openmart"|"google_maps" to pin to one. Pricing is returned in the response credit headers. Results do NOT include review text — to fetch a place\'s reviews, pass its `url` to get_place_reviews.';
6
6
  export const searchPlacesSchema = {
7
7
  query: z.string().optional().describe('Free-text query (e.g. "coffee shops in Brooklyn", "law firm New York"). Used by both providers.'),
8
8
  country: z.string().optional().describe('ISO country code. Openmart accepts US/CA/AU/PR/NZ only — outside this set, routes to Google Maps. Auto-cased per provider.'),
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export declare const searchRedditName = "search_reddit";
3
- export declare const searchRedditDescription = "Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30\u2013120s). Cost: 1 credit per item returned.";
3
+ export declare const searchRedditDescription = "Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30\u2013120s). Pricing is returned in the response credit headers.";
4
4
  export declare const searchRedditSchema: {
5
5
  start_urls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
6
6
  query: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"search-reddit.d.ts","sourceRoot":"","sources":["../../src/tools/search-reddit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,gBAAgB,kBAAkB,CAAA;AAE/C,eAAO,MAAM,uBAAuB,kTACwQ,CAAA;AAE5S,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAwB9B,CAAA;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;GAEvE"}
1
+ {"version":3,"file":"search-reddit.d.ts","sourceRoot":"","sources":["../../src/tools/search-reddit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,gBAAgB,kBAAkB,CAAA;AAE/C,eAAO,MAAM,uBAAuB,oUAC0R,CAAA;AAE9T,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;CAwB9B,CAAA;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;GAEvE"}
@@ -2,7 +2,7 @@ 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 searchRedditName = 'search_reddit';
5
- export const searchRedditDescription = 'Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30–120s). Cost: 1 credit per item returned.';
5
+ export const searchRedditDescription = 'Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30–120s). Pricing is returned in the response credit headers.';
6
6
  export const searchRedditSchema = {
7
7
  start_urls: z.array(z.string().url()).max(25).optional()
8
8
  .describe('Reddit URLs to scrape (subreddit, post, user, or search URL). Up to 25. Provide this and/or query. Example: ["https://www.reddit.com/r/sales/"]'),
@@ -17,7 +17,7 @@ export const searchRedditSchema = {
17
17
  time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).optional()
18
18
  .describe('Time filter.'),
19
19
  limit: z.number().int().min(1).max(200).default(10)
20
- .describe('Max items to return (1–200). 1 credit per item.'),
20
+ .describe('Max items to return (1–200). Pricing is returned in the response credit headers.'),
21
21
  max_comments: z.number().int().min(0).max(1000).optional()
22
22
  .describe('Max comments to fetch per post. Set 0 to skip comments.'),
23
23
  include_comments: z.boolean().optional()
@@ -1 +1 @@
1
- {"version":3,"file":"search-reddit.js","sourceRoot":"","sources":["../../src/tools/search-reddit.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,gBAAgB,GAAG,eAAe,CAAA;AAE/C,MAAM,CAAC,MAAM,uBAAuB,GAClC,0SAA0S,CAAA;AAE5S,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;SACrD,QAAQ,CAAC,iJAAiJ,CAAC;IAC9J,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACzB,QAAQ,CAAC,sUAAsU,CAAC;IACnV,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;SAChF,QAAQ,CAAC,wEAAwE,CAAC;IACrF,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACzC,QAAQ,CAAC,+DAA+D,CAAC;IAC5E,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE;SAC9E,QAAQ,CAAC,yBAAyB,CAAC;IACtC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE;SACrE,QAAQ,CAAC,cAAc,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;SAChD,QAAQ,CAAC,iDAAiD,CAAC;IAC9D,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;SACvD,QAAQ,CAAC,yDAAyD,CAAC;IACtE,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;SACrC,QAAQ,CAAC,mCAAmC,CAAC;IAChD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACnC,QAAQ,CAAC,kFAAkF,CAAC;IAC/F,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACtC,QAAQ,CAAC,qFAAqF,CAAC;IAClG,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CACnW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAA8B;IACtE,OAAO,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAC1C,CAAC"}
1
+ {"version":3,"file":"search-reddit.js","sourceRoot":"","sources":["../../src/tools/search-reddit.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,gBAAgB,GAAG,eAAe,CAAA;AAE/C,MAAM,CAAC,MAAM,uBAAuB,GAClC,4TAA4T,CAAA;AAE9T,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;SACrD,QAAQ,CAAC,iJAAiJ,CAAC;IAC9J,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACzB,QAAQ,CAAC,sUAAsU,CAAC;IACnV,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;SAChF,QAAQ,CAAC,wEAAwE,CAAC;IACrF,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACzC,QAAQ,CAAC,+DAA+D,CAAC;IAC5E,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE;SAC9E,QAAQ,CAAC,yBAAyB,CAAC;IACtC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE;SACrE,QAAQ,CAAC,cAAc,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;SAChD,QAAQ,CAAC,kFAAkF,CAAC;IAC/F,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;SACvD,QAAQ,CAAC,yDAAyD,CAAC;IACtE,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;SACrC,QAAQ,CAAC,mCAAmC,CAAC;IAChD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACnC,QAAQ,CAAC,kFAAkF,CAAC;IAC/F,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACtC,QAAQ,CAAC,qFAAqF,CAAC;IAClG,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gLAAgL,yBAAyB,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qEAAqE,CAAC;CACnW,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAA8B;IACtE,OAAO,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAC1C,CAAC"}
@@ -9,7 +9,7 @@ export const verifyEmailsBulkDescription = 'Verify up to 5,000 emails in one ASY
9
9
  'means the address was SKIPPED (rate-limited / never checked) and is safe to re-submit — do not discard those contacts; ' +
10
10
  'only `provider_indeterminate` (or no reason) is a real "couldn\'t determine". ' +
11
11
  'The 202 response includes `credits_reserved` (worst-case hold, settled to actual on completion) and a `low_balance_warning` when low. ' +
12
- 'PROVIDER: runs on BounceBan by default — it resolves catch-all/risky addresses without sending (~0.2 credits per conclusively-verified email). If the BounceBan submit fails, the job AUTOMATICALLY falls back to Icypeas (0.1 credits/verified, no catch-all resolution) — bulk verify survives a single-provider outage. Pinning use_providers:["bounceban"] or ["icypeas"] is STRICT: a pinned backend\'s failure returns an error instead of switching. Any other verifier returns a clear error (never silently ignored). ' +
12
+ 'PROVIDER: runs on BounceBan by default — it resolves catch-all/risky addresses without sending. If the BounceBan submit fails, the job AUTOMATICALLY falls back to Icypeas (no catch-all resolution) — bulk verify survives a single-provider outage. Pinning use_providers:["bounceban"] or ["icypeas"] is STRICT: a pinned backend\'s failure returns an error instead of switching. Any other verifier returns a clear error (never silently ignored). ' +
13
13
  'For a single email use verify_email instead.';
14
14
  export const verifyEmailsBulkSchema = {
15
15
  emails: z.array(z.string()).min(1).max(5000).describe('Emails to verify (1–5000). Invalid/duplicate entries are dropped server-side.'),
@@ -1 +1 @@
1
- {"version":3,"file":"verify-emails-bulk.js","sourceRoot":"","sources":["../../src/tools/verify-emails-bulk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,oBAAoB,CAAA;AAExD,MAAM,CAAC,MAAM,2BAA2B,GACtC,mHAAmH;IACnH,sIAAsI;IACtI,2HAA2H;IAC3H,4GAA4G;IAC5G,gIAAgI;IAChI,yHAAyH;IACzH,gFAAgF;IAChF,wIAAwI;IACxI,igBAAigB;IACjgB,8CAA8C,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,+EAA+E,CAAC;IACtI,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gTAAgT,CAAC;IACxW,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;CACjG,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,KAA8B;IAC1E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,2BAA2B,EAAE,KAAK,CAAC,CAAA;IACrE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAA;AACnG,CAAC"}
1
+ {"version":3,"file":"verify-emails-bulk.js","sourceRoot":"","sources":["../../src/tools/verify-emails-bulk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,oBAAoB,CAAA;AAExD,MAAM,CAAC,MAAM,2BAA2B,GACtC,mHAAmH;IACnH,sIAAsI;IACtI,2HAA2H;IAC3H,4GAA4G;IAC5G,gIAAgI;IAChI,yHAAyH;IACzH,gFAAgF;IAChF,wIAAwI;IACxI,4bAA4b;IAC5b,8CAA8C,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,+EAA+E,CAAC;IACtI,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gTAAgT,CAAC;IACxW,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;CACjG,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,KAA8B;IAC1E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,2BAA2B,EAAE,KAAK,CAAC,CAAA;IACrE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAA;AACnG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ import { enrichPersonBulkName, enrichPersonBulkDescription, enrichPersonBulkSche
53
53
  import { enrichCompanyBulkName, enrichCompanyBulkDescription, enrichCompanyBulkSchema, enrichCompanyBulkHandler } from './tools/enrich-company-bulk.js'
54
54
  import { findPhoneBulkName, findPhoneBulkDescription, findPhoneBulkSchema, findPhoneBulkHandler } from './tools/find-phone-bulk.js'
55
55
  import { getBulkJobName, getBulkJobDescription, getBulkJobSchema, getBulkJobHandler } from './tools/get-bulk-job.js'
56
+ import { cancelBulkJobName, cancelBulkJobDescription, cancelBulkJobSchema, cancelBulkJobHandler } from './tools/cancel-bulk-job.js'
56
57
  import { searchEndpointsName, searchEndpointsDescription, searchEndpointsSchema, searchEndpointsHandler } from './tools/search-endpoints.js'
57
58
  import { getEndpointDetailsName, getEndpointDetailsDescription, getEndpointDetailsSchema, getEndpointDetailsHandler } from './tools/get-endpoint-details.js'
58
59
  import { callEndpointName, callEndpointDescription, callEndpointSchema, callEndpointHandler } from './tools/call-endpoint.js'
@@ -226,6 +227,7 @@ server.tool(enrichPersonBulkName, enrichPersonBulkDescription, enrichPersonBulkS
226
227
  server.tool(enrichCompanyBulkName, enrichCompanyBulkDescription, enrichCompanyBulkSchema, enrichCompanyBulkHandler)
227
228
  server.tool(findPhoneBulkName, findPhoneBulkDescription, findPhoneBulkSchema, findPhoneBulkHandler)
228
229
  server.tool(getBulkJobName, getBulkJobDescription, getBulkJobSchema, getBulkJobHandler)
230
+ server.tool(cancelBulkJobName, cancelBulkJobDescription, cancelBulkJobSchema, cancelBulkJobHandler)
229
231
  server.tool(findPhoneName, findPhoneDescription, findPhoneSchema, findPhoneHandler)
230
232
  server.tool(enrichCompanyName, enrichCompanyDescription, enrichCompanySchema, enrichCompanyHandler)
231
233
  server.tool(searchWebName, searchWebDescription, searchWebSchema, searchWebHandler)
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod'
2
+ import { callApi } from '../client.js'
3
+
4
+ export const cancelBulkJobName = 'cancel_bulk_job'
5
+
6
+ export const cancelBulkJobDescription =
7
+ 'Stop a RUNNING bulk job (find_emails_bulk, enrich_person_bulk, enrich_company_bulk, find_phone_bulk) so it spends no more credits — the brake for a job that is chewing through a large list. ' +
8
+ 'Cancels FUTURE row processing only: rows already found/enriched (and charged) stay — you keep those results — and one in-flight batch (≤ ~25 rows) may still complete. ' +
9
+ 'Un-started rows come back from get_bulk_job as coverage.skipped with result.reason "cancelled" and are safe to re-submit later. ' +
10
+ 'Idempotent: cancelling a job that already finished (or was already cancelled) just returns its current status, not an error. ' +
11
+ 'NOTE: verify_emails_bulk jobs run as one provider-side batch that finishes in minutes with the cost reserved up front, so they cannot be cancelled midway (returns an error) — just let them finish. ' +
12
+ 'Returns { job_id, status, items_total, items_processed, items_cancelled }.'
13
+
14
+ export const cancelBulkJobSchema = {
15
+ job_id: z.number().int().describe('The job_id returned by a *_bulk submit tool.'),
16
+ }
17
+
18
+ export async function cancelBulkJobHandler(input: Record<string, unknown>) {
19
+ const { job_id } = input
20
+ const res = await callApi('POST', `/jobs/${job_id}/cancel`, {})
21
+ return { content: [{ type: 'text' as const, text: JSON.stringify(res.data) }], isError: !res.ok }
22
+ }
@@ -7,7 +7,8 @@ export const enrichCompanyBulkDescription =
7
7
  'Enrich up to 5,000 companies (size, industry, funding, technologies, social profiles) in one ASYNC job. Returns a job_id immediately — this does NOT return firmographics inline. ' +
8
8
  'Poll get_bulk_job(job_id) until status is "done" (drains over a few minutes), then page through rows. ' +
9
9
  'Each row runs the same managed waterfall as enrich_company; provide at least one of domain, linkedin_url, or name per company. ' +
10
- 'Charged only per row a provider enriches (misses are free). For a single company, use enrich_company instead.'
10
+ 'Charged only per row a provider enriches (misses are free). For a single company, use enrich_company instead. ' +
11
+ 'Call cancel_bulk_job to stop a running job early — already-processed rows stay charged, un-started rows are never billed.'
11
12
 
12
13
  export const enrichCompanyBulkSchema = {
13
14
  inputs: z
@@ -7,7 +7,8 @@ export const enrichPersonBulkDescription =
7
7
  'Enrich up to 5,000 people (LinkedIn profile + professional details) in one ASYNC job. Returns a job_id immediately — this does NOT return profiles inline. ' +
8
8
  'Poll get_bulk_job(job_id) until status is "done" (drains over a few minutes), then page through rows. ' +
9
9
  'Each row runs the same managed waterfall as enrich_person; provide an email, a linkedin_url, or name + company/domain per person. ' +
10
- 'Charged only per row a provider enriches (misses are free). For a single person, use enrich_person instead.'
10
+ 'Charged only per row a provider enriches (misses are free). For a single person, use enrich_person instead. ' +
11
+ 'Call cancel_bulk_job to stop a running job early — already-processed rows stay charged, un-started rows are never billed.'
11
12
 
12
13
  export const enrichPersonBulkSchema = {
13
14
  inputs: z
@@ -6,7 +6,7 @@ export const extractPostEngagementName = 'extract_post_engagement'
6
6
  export const extractPostEngagementDescription =
7
7
  'Extract the people who engaged with a LinkedIn post — commenters and/or reactors — as a deduplicated list of contacts (name, profile URL, headline). ' +
8
8
  'Use this for social-signal prospecting: pull everyone who engaged with a viral post, then chain the results into enrich_person / find_email to get roles and work emails. ' +
9
- 'Runs an async extraction job (typically ~30–120s) and returns once the people are ready. Costs 10 credits per post.'
9
+ 'Runs an async extraction job (typically ~30–120s) and returns once the people are ready. Pricing is returned in the response credit headers.'
10
10
 
11
11
  export const extractPostEngagementSchema = {
12
12
  post_url: z
@@ -50,7 +50,7 @@ export async function extractPostEngagementHandler(input: Record<string, unknown
50
50
  const dataTypes =
51
51
  type === 'comments' ? ['comment'] : type === 'reactions' ? ['reaction'] : ['comment', 'reaction']
52
52
 
53
- // Step 1 — create the extraction task. Billing (10 credits) happens here.
53
+ // Step 1 — create the extraction task. Billing happens here.
54
54
  const createRes = await callApi('POST', '/jungler/workbooks', {
55
55
  post_url: postUrl,
56
56
  data_types: dataTypes,
@@ -8,7 +8,7 @@ export const fetchPageContentDescription =
8
8
  'Fetch the full text content of one or more web pages given their URLs. ' +
9
9
  'Returns extracted page text suitable for RAG, summarisation, or fact extraction. ' +
10
10
  'Use search_web first to discover URLs, then use this tool to fetch their full text. ' +
11
- 'Costs 1 credit per URL submitted.'
11
+ 'Pricing is returned in the response credit headers.'
12
12
 
13
13
  export const fetchPageContentSchema = {
14
14
  urls: z
@@ -16,7 +16,7 @@ export const findEmailSchema = {
16
16
  domain: z.string().optional().describe('Company domain (e.g. "stripe.com")'),
17
17
  company_name: z.string().optional().describe('Company name — alternative to domain'),
18
18
  linkedin_url: z.string().optional().describe('LinkedIn profile URL — alternative to name+domain'),
19
- resolve_current_employer: z.boolean().optional().describe('Opt-in: on a miss with a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). Extra ~1 credit for the enrich hop, disclosed in _meta.employer_resolution. Default false.'),
19
+ resolve_current_employer: z.boolean().optional().describe('Opt-in: on a miss with a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). The enrich hop is billed and disclosed in _meta.employer_resolution. Default false.'),
20
20
  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('find_email').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
21
21
  max_credits: z.number().positive().optional().describe('Per-record spend cap: a provider that would find the email but costs more than this is skipped, so you never pay more than N credits for one email (returns not_found if nothing answers in budget). Stops a high-cost provider from dominating a large run.'),
22
22
  }
@@ -13,7 +13,8 @@ export const findEmailsBulkDescription =
13
13
  'result carries a `confidence` (high/medium/unknown) — treat medium/unknown as unverified and run ' +
14
14
  'verify_emails_bulk before sending, or you risk bounces. A located email the provider itself flags as a low-confidence ' +
15
15
  'pattern guess comes back as verdict `guessed` instead of `found` and is FREE — never send a `guessed` email unverified. ' +
16
- 'For 50 or fewer people that you want back inline, use find_emails instead.'
16
+ 'For 50 or fewer people that you want back inline, use find_emails instead. ' +
17
+ 'Started a job that is too big or wrong? Call cancel_bulk_job to stop it — already-processed rows stay charged, un-started rows are never billed.'
17
18
 
18
19
  export const findEmailsBulkSchema = {
19
20
  people: z
@@ -30,7 +31,7 @@ export const findEmailsBulkSchema = {
30
31
  .min(1)
31
32
  .max(5000)
32
33
  .describe('People to find emails for (1–5000). Searchable = linkedin_url alone, OR first_name + last_name + (domain or company_name).'),
33
- resolve_current_employer: z.boolean().optional().describe('Opt-in, applies to every row: when a row misses and has a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). Extra ~1 credit per resolved row. Default false.'),
34
+ resolve_current_employer: z.boolean().optional().describe('Opt-in, applies to every row: when a row misses and has a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). Each enrich hop is billed. Default false.'),
34
35
  webhook_url: z.string().optional().describe('Optional URL POSTed when the whole job completes.'),
35
36
  max_credits: z.number().positive().optional().describe('Per-row spend cap: a provider that would find the email but costs more than this is skipped (the row comes back not_found rather than overspending). Caps high-cost providers across the whole list.'),
36
37
  }
@@ -30,7 +30,7 @@ export const findEmailsSchema = {
30
30
  .min(1)
31
31
  .max(50)
32
32
  .describe('People to find emails for (max 50)'),
33
- resolve_current_employer: z.boolean().optional().describe('Opt-in, applies to every person in the batch: on a miss with a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). Extra ~1 credit per resolved person. Default false.'),
33
+ resolve_current_employer: z.boolean().optional().describe('Opt-in, applies to every person in the batch: on a miss with a linkedin_url, enrich the person to resolve their CURRENT employer\'s domain and retry once — recovers stale/acquired input domains (e.g. Atmel→microchip.com). Each enrich hop is billed. Default false.'),
34
34
  use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically run the best waterfall — recommended for most use cases. Available providers: ${FIND_EMAILS_PROVIDERS.join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
35
35
  max_credits: z.number().positive().optional().describe('Per-person spend cap applied to every person in the batch: a provider that would find the email but costs more than this is skipped, so you never pay more than N credits per email. Stops a high-cost provider from dominating a large run.'),
36
36
  }
@@ -5,14 +5,14 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
5
5
  export const findInfluencersName = 'find_influencers'
6
6
 
7
7
  export const findInfluencersDescription =
8
- 'Discover and find influencers/creators on Instagram, YouTube, TikTok, Twitch, Twitter, and OnlyFans via 2 providers (Influencers Club Similar, Influencers Club Discovery). Routes by input: handle set → lookalike search (influencers_similar) runs first; no handle → keyword/filter discovery. Filters: location, gender, type (creator/business), AI natural language search, sort. Cost: 1 credit per result returned. ' +
8
+ 'Discover and find influencers/creators on Instagram, YouTube, TikTok, Twitch, Twitter, and OnlyFans via 2 providers (Influencers Club Similar, Influencers Club Discovery). Routes by input: handle set → lookalike search (influencers_similar) runs first; no handle → keyword/filter discovery. Filters: location, gender, type (creator/business), AI natural language search, sort. Pricing is returned in the response credit headers. ' +
9
9
  'LIMITATIONS: LinkedIn is not a supported platform (the underlying creator index has no LinkedIn coverage) — for B2B/LinkedIn prospecting use extract_post_engagement to pull engagers off a specific LinkedIn post instead. There is no follower-count range filter; to bias toward a follower tier, set sort_by="number_of_followers" and filter the returned list client-side.'
10
10
 
11
11
  export const findInfluencersSchema = {
12
12
  platform: z.enum(['instagram', 'youtube', 'tiktok', 'twitch', 'twitter', 'onlyfans'])
13
13
  .describe('Target platform.'),
14
14
  limit: z.number().int().min(1).max(100).default(25)
15
- .describe('Max creators to return (1–100). 1 credit per result.'),
15
+ .describe('Max creators to return (1–100). Pricing is returned in the response credit headers.'),
16
16
  page: z.number().int().min(1).default(1).optional()
17
17
  .describe('Page number for pagination.'),
18
18
  sort_by: z.enum(['relevancy', 'number_of_followers', 'engagement_rate']).optional(),
@@ -16,7 +16,7 @@ export const findPeopleDescription =
16
16
  'Do NOT pass seniorities unless explicitly asked — job titles alone produce better results. ' +
17
17
  'Returns names, titles, LinkedIn URLs, plus company name/domain/linkedin/headcount when available. Default limit: 25. ' +
18
18
  'Response is compact by default (~99% smaller than the raw provider payload). Set fields="verbose" only when you specifically need employment history, skills, languages, or company descriptions. ' +
19
- 'When the matched provider is Apollo, last names are partially obfuscated and emails/LinkedIn URLs are omitted unless reveal=true is passed (adds 1 credit per revealed person).'
19
+ 'When the matched provider is Apollo, last names are partially obfuscated and emails/LinkedIn URLs are omitted unless reveal=true is passed (adds a billed reveal per person).'
20
20
 
21
21
  export const findPeopleSchema = {
22
22
  company_linkedin_urls: z.array(z.string()).optional().describe('Company LinkedIn URLs (preferred over domains when available, e.g. ["https://linkedin.com/company/microsoft"])'),
@@ -28,7 +28,7 @@ export const findPeopleSchema = {
28
28
  limit: z.number().min(1).max(500).default(25).describe('Max results across all companies combined (default: 25, max: 500). For multiple companies multiply: e.g. 10 companies × 5 each = 50. Note: when the matched provider is Apollo, results are capped at 100 per call (Apollo upstream limit).'),
29
29
  max_per_company: z.number().min(1).optional().describe('Cap the number of contacts returned per company (e.g. 3 for a buying committee). Applied across all matched companies, before the global limit.'),
30
30
  fields: z.enum(['compact', 'verbose']).default('compact').describe('Response shape. "compact" (default) returns a small, predictable per-person record: name, title, linkedin_url, company {name, domain, linkedin_url, headcount}, plus seniority/email/location when the upstream provider includes them. "verbose" returns the raw provider passthrough (~30KB+ per record from FullEnrich: employment history, every company office, descriptions, skills, languages) — only use when you specifically need those fields.'),
31
- reveal: z.boolean().optional().describe('Apollo-only: when true, follow the obfuscated Apollo people-search results with /apollo/people/bulk-match to reveal full names, emails, and LinkedIn URLs. Costs 1 extra credit per revealed person on top of the search credit. Has no effect for other providers (their data is already unobfuscated). Default false to protect credits.'),
31
+ reveal: z.boolean().optional().describe('Apollo-only: when true, follow the obfuscated Apollo people-search results with /apollo/people/bulk-match to reveal full names, emails, and LinkedIn URLs. Each reveal is billed; the response credit headers report the amount. Has no effect for other providers (their data is already unobfuscated). Default false to protect credits.'),
32
32
  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('find_people').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
33
33
  }
34
34
 
@@ -7,7 +7,8 @@ export const findPhoneBulkDescription =
7
7
  'Find direct/mobile phone numbers for up to 5,000 people in one ASYNC job. Returns a job_id immediately — this does NOT return numbers inline. ' +
8
8
  'Poll get_bulk_job(job_id) until status is "done" (drains over a few minutes), then page through rows. ' +
9
9
  'Each row runs the same managed waterfall as find_phone; provide a linkedin_url, or first+last name with company_domain/company_name. ' +
10
- 'Charged only per row a provider returns a number (misses are free). Phone lookups are EXPENSIVE (10+ credits each) — scope the list deliberately. For a single person, use find_phone instead.'
10
+ 'Charged only per row a provider returns a number (misses are free). Phone lookups are relatively expensive — scope the list deliberately. For a single person, use find_phone instead. ' +
11
+ 'Overshot the list? Call cancel_bulk_job to stop it immediately — already-processed rows stay charged, un-started rows are never billed.'
11
12
 
12
13
  export const findPhoneBulkSchema = {
13
14
  inputs: z
@@ -24,7 +25,7 @@ export const findPhoneBulkSchema = {
24
25
  .max(5000)
25
26
  .describe('People to find phone numbers for (1–5000). Each needs a linkedin_url, or name + company_domain/name.'),
26
27
  webhook_url: z.string().optional().describe('Optional URL POSTed when the whole job completes.'),
27
- max_credits: z.number().positive().optional().describe('Per-row spend cap: a provider that would return a number but costs more than this is skipped (the row comes back not_found rather than overspending). Phone lookups can be 10+ credits — this bounds them.'),
28
+ max_credits: z.number().positive().optional().describe('Per-row spend cap: a provider that would return a number but costs more than this is skipped (the row comes back not_found rather than overspending).'),
28
29
  }
29
30
 
30
31
  export async function findPhoneBulkHandler(input: Record<string, unknown>) {
@@ -5,7 +5,7 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
5
5
  export const findPhoneName = 'find_phone'
6
6
 
7
7
  export const findPhoneDescription =
8
- 'Find a direct phone number for a person. Accepts a LinkedIn URL, or first+last name with company domain/name. Phone lookups are expensive (10+ credits) — use only when specifically needed.'
8
+ 'Find a direct phone number for a person. Accepts a LinkedIn URL, or first+last name with company domain/name. Phone lookups are relatively expensive — use only when specifically needed.'
9
9
 
10
10
  export const findPhoneSchema = {
11
11
  linkedin_url: z.string().url().optional().describe('LinkedIn profile URL'),
@@ -14,7 +14,7 @@ export const findPhoneSchema = {
14
14
  company_domain: z.string().optional().describe('Company domain, e.g. coldiq.com'),
15
15
  company_name: z.string().optional().describe('Company name (alternative to company_domain)'),
16
16
  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('find_phone').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
17
- max_credits: z.number().positive().optional().describe('Per-record spend cap: a provider that would return a number but costs more than this is skipped, so you never pay more than N credits for one phone (returns not_found if nothing answers in budget). Phone lookups can be 10+ credits — this bounds them.'),
17
+ max_credits: z.number().positive().optional().describe('Per-record spend cap: a provider that would return a number but costs more than this is skipped, so you never pay more than N credits for one phone (returns not_found if nothing answers in budget).'),
18
18
  }
19
19
 
20
20
  export async function findPhoneHandler(input: Record<string, unknown>) {
@@ -5,14 +5,15 @@ export const getBulkJobName = 'get_bulk_job'
5
5
 
6
6
  export const getBulkJobDescription =
7
7
  'Poll any async bulk job (verify_emails_bulk, find_emails_bulk, enrich_person_bulk, enrich_company_bulk, find_phone_bulk). ' +
8
- 'Returns { status, items_total, items_processed, summary, coverage, credits_reserved, credits_charged, page }. Poll until status is "done" (or "timed_out"). ' +
8
+ 'Returns { status, items_total, items_processed, summary, coverage, credits_reserved, credits_charged, page }. Poll until status is TERMINAL — one of "done", "timed_out", "failed", or "cancelled" (a job someone stopped with cancel_bulk_job); stop polling on any of these. ' +
9
9
  'Use the `summary` per-verdict counts for totals — do NOT expect all rows inline: rows come one page at a time (default 100). ' +
10
10
  'For find jobs, verdict `guessed` means an email was located but the provider marked it a low-confidence pattern guess — it is NOT billed; verify it before sending. ' +
11
11
  'The `coverage` block {conclusive, skipped, unverifiable, failed, pending} tells you how much was truly resolved: `skipped` rows (per-row ' +
12
12
  '`result.reason` chunk_failed/not_returned/timed_out) were never conclusively checked and are safe to re-submit; `failed` is a hard error (retry unlikely to help). ' +
13
13
  '`credits_charged` (null until settled) is the actual spend; `credits_reserved` is the worst-case hold. Each row echoes its submitted `input`, ' +
14
14
  'so you can map verdicts back to your submissions without tracking order. To page, OMIT cursor for the first page, then call again with ' +
15
- 'cursor = page.next_cursor until next_cursor is null.'
15
+ 'cursor = page.next_cursor until next_cursor is null. ' +
16
+ 'To STOP a running find/enrich/phone job early (before it spends more credits), call cancel_bulk_job — already-processed rows stay charged, un-started rows are never billed.'
16
17
 
17
18
  export const getBulkJobSchema = {
18
19
  job_id: z.number().int().describe('The job_id returned by a *_bulk submit tool.'),
@@ -7,7 +7,7 @@ export const getPlaceReviewsName = 'get_place_reviews'
7
7
  export const getPlaceReviewsDescription =
8
8
  'Fetch Google Maps reviews for one or more places. Pass the Google Maps place URLs (from search_places results, the `url` field) and get back each place\'s reviews — useful for reputation management, local-services prospecting, and surfacing negative-review signals. ' +
9
9
  'search_places returns place listings WITHOUT review text; use this tool to get the actual review content. ' +
10
- 'Runs an async job (~30–120s). Cost: 1 credit per review returned.'
10
+ 'Runs an async job (~30–120s). Pricing is returned in the response credit headers.'
11
11
 
12
12
  export const getPlaceReviewsSchema = {
13
13
  place_urls: z
@@ -21,7 +21,7 @@ export const getPlaceReviewsSchema = {
21
21
  .min(1)
22
22
  .max(300)
23
23
  .optional()
24
- .describe('Maximum reviews to fetch per place (default 5, max 300). Each returned review costs 1 credit, so keep this tight.'),
24
+ .describe('Maximum reviews to fetch per place (default 5, max 300). Pricing is returned in the response credit headers, so keep this tight.'),
25
25
  sort: z
26
26
  .enum(['mostRelevant', 'newest', 'highestRanking', 'lowestRanking'])
27
27
  .optional()
@@ -5,7 +5,7 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
5
5
  export const searchAdsName = 'search_ads'
6
6
 
7
7
  export const searchAdsDescription =
8
- 'Search live ad creatives across 4 ad libraries (Google Ads Transparency, LinkedIn Ad Library, Meta Ads Library, Twitter/X Ads) — a high-signal GTM input for competitive intelligence, ICP refinement, and pitch personalization. Routes by input: domains/advertiser_ids → Google only; search_urls → LinkedIn only; bare query → Google → Meta → Twitter waterfall. Use platform="google"|"linkedin"|"meta"|"twitter" to pin to one platform. All providers are async (~10–60s). Cost: ~5 credits per call (Twitter charges 1 credit per ad returned). Credits are fully refunded when a run returns zero ads. NOTE: Google Ads creatives return image URLs + creative IDs, not ad copy text — open the image URLs to read the ad. There is no "currently running only" filter; results can span past campaigns.'
8
+ 'Search live ad creatives across 4 ad libraries (Google Ads Transparency, LinkedIn Ad Library, Meta Ads Library, Twitter/X Ads) — a high-signal GTM input for competitive intelligence, ICP refinement, and pitch personalization. Routes by input: domains/advertiser_ids → Google only; search_urls → LinkedIn only; bare query → Google → Meta → Twitter waterfall. Use platform="google"|"linkedin"|"meta"|"twitter" to pin to one platform. All providers are async (~10–60s). Pricing is returned in the response credit headers; credits are fully refunded when a run returns zero ads. NOTE: Google Ads creatives return image URLs + creative IDs, not ad copy text — open the image URLs to read the ad. There is no "currently running only" filter; results can span past campaigns.'
9
9
 
10
10
  export const searchAdsSchema = {
11
11
  query: z.string().optional().describe('Advertiser/company name or keyword. Routes to Google→Meta→Twitter when no platform-specific input is set.'),
@@ -16,7 +16,7 @@ export const searchAdsSchema = {
16
16
  search_urls: z.array(z.string()).optional().describe('Pre-built LinkedIn Ad Library URLs from linkedin.com/ad-library. Routes to LinkedIn only. Build at linkedin.com/ad-library/search using accountOwner/countries/dateOption filters.'),
17
17
 
18
18
  country: z.string().optional().describe('ISO country code (2-letter for Meta/Twitter; longer accepted by Google as region).'),
19
- max_results: z.number().int().min(1).max(200).default(25).describe('Max ads per provider call (1–200). Per-provider caps: Google 1000, LinkedIn 200, Meta 200, Twitter 100. Twitter charges 1 credit per ad returned.'),
19
+ max_results: z.number().int().min(1).max(200).default(25).describe('Max ads per provider call (1–200). Per-provider caps: Google 1000, LinkedIn 200, Meta 200, Twitter 100. Pricing is returned in the response credit headers.'),
20
20
 
21
21
  ad_type: z.enum(['ALL', 'POLITICAL_AND_ISSUE_ADS']).optional().describe('Meta only. Defaults to ALL.'),
22
22
 
@@ -5,7 +5,7 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
5
5
  export const searchPlacesName = 'search_places'
6
6
 
7
7
  export const searchPlacesDescription =
8
- 'Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) — useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} → Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30–120s) as fallback or for global coverage. Use provider="openmart"|"google_maps" to pin to one. Cost: 1 credit per place returned (both providers). Results do NOT include review text — to fetch a place\'s reviews, pass its `url` to get_place_reviews.'
8
+ 'Search local businesses and places via 2 providers (Openmart Search, Google Maps Scraper) — useful for territory mapping, local-services prospecting, restaurant/retail/vertical research. Routes by input: structured filters or country in {US,CA,AU,PR,NZ} → Openmart (sync, ~1s) first, then Google Maps Scraper (async, ~30–120s) as fallback or for global coverage. Use provider="openmart"|"google_maps" to pin to one. Pricing is returned in the response credit headers. Results do NOT include review text — to fetch a place\'s reviews, pass its `url` to get_place_reviews.'
9
9
 
10
10
  export const searchPlacesSchema = {
11
11
  query: z.string().optional().describe('Free-text query (e.g. "coffee shops in Brooklyn", "law firm New York"). Used by both providers.'),
@@ -5,7 +5,7 @@ import { getProvidersForCapability } from '../utils/provider-resolver.js'
5
5
  export const searchRedditName = 'search_reddit'
6
6
 
7
7
  export const searchRedditDescription =
8
- 'Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30–120s). Cost: 1 credit per item returned.'
8
+ 'Scrape Reddit posts, comments, communities, or users via 1 provider (Reddit Scraper). Provide subreddit/post/user URLs and/or a keyword query. Optionally scope a query to one community, sort, filter by time, include comments, and limit by date. Async (~30–120s). Pricing is returned in the response credit headers.'
9
9
 
10
10
  export const searchRedditSchema = {
11
11
  start_urls: z.array(z.string().url()).max(25).optional()
@@ -21,7 +21,7 @@ export const searchRedditSchema = {
21
21
  time: z.enum(['hour', 'day', 'week', 'month', 'year', 'all']).optional()
22
22
  .describe('Time filter.'),
23
23
  limit: z.number().int().min(1).max(200).default(10)
24
- .describe('Max items to return (1–200). 1 credit per item.'),
24
+ .describe('Max items to return (1–200). Pricing is returned in the response credit headers.'),
25
25
  max_comments: z.number().int().min(0).max(1000).optional()
26
26
  .describe('Max comments to fetch per post. Set 0 to skip comments.'),
27
27
  include_comments: z.boolean().optional()
@@ -12,7 +12,7 @@ export const verifyEmailsBulkDescription =
12
12
  'means the address was SKIPPED (rate-limited / never checked) and is safe to re-submit — do not discard those contacts; ' +
13
13
  'only `provider_indeterminate` (or no reason) is a real "couldn\'t determine". ' +
14
14
  'The 202 response includes `credits_reserved` (worst-case hold, settled to actual on completion) and a `low_balance_warning` when low. ' +
15
- 'PROVIDER: runs on BounceBan by default — it resolves catch-all/risky addresses without sending (~0.2 credits per conclusively-verified email). If the BounceBan submit fails, the job AUTOMATICALLY falls back to Icypeas (0.1 credits/verified, no catch-all resolution) — bulk verify survives a single-provider outage. Pinning use_providers:["bounceban"] or ["icypeas"] is STRICT: a pinned backend\'s failure returns an error instead of switching. Any other verifier returns a clear error (never silently ignored). ' +
15
+ 'PROVIDER: runs on BounceBan by default — it resolves catch-all/risky addresses without sending. If the BounceBan submit fails, the job AUTOMATICALLY falls back to Icypeas (no catch-all resolution) — bulk verify survives a single-provider outage. Pinning use_providers:["bounceban"] or ["icypeas"] is STRICT: a pinned backend\'s failure returns an error instead of switching. Any other verifier returns a clear error (never silently ignored). ' +
16
16
  'For a single email use verify_email instead.'
17
17
 
18
18
  export const verifyEmailsBulkSchema = {
@@ -8,6 +8,7 @@ vi.mock('../src/client.js', () => ({
8
8
  import { verifyEmailsBulkHandler } from '../src/tools/verify-emails-bulk.js'
9
9
  import { findEmailsBulkHandler } from '../src/tools/find-emails-bulk.js'
10
10
  import { getBulkJobHandler } from '../src/tools/get-bulk-job.js'
11
+ import { cancelBulkJobHandler } from '../src/tools/cancel-bulk-job.js'
11
12
 
12
13
  beforeEach(() => {
13
14
  callApiMock.mockReset()
@@ -45,4 +46,20 @@ describe('bulk MCP tools', () => {
45
46
  const res = await verifyEmailsBulkHandler({ emails: ['a@x.com'] })
46
47
  expect(res.isError).toBe(true)
47
48
  })
49
+
50
+ it('cancel_bulk_job POSTs job_id in the path (empty body) to the cancel endpoint', async () => {
51
+ callApiMock.mockResolvedValue({ ok: true, status: 200, data: { job_id: 42, status: 'cancelled', items_total: 50, items_processed: 12 }, headers: {} })
52
+ const res = await cancelBulkJobHandler({ job_id: 42 })
53
+ const [method, path, body] = callApiMock.mock.calls[0]
54
+ expect(method).toBe('POST')
55
+ expect(path).toBe('/jobs/42/cancel')
56
+ expect(body).toEqual({})
57
+ expect(JSON.parse(res.content[0].text)).toMatchObject({ status: 'cancelled' })
58
+ })
59
+
60
+ it('cancel_bulk_job surfaces isError on a 400 (not cancellable)', async () => {
61
+ callApiMock.mockResolvedValue({ ok: false, status: 400, data: { error: "can't be cancelled" }, headers: {} })
62
+ const res = await cancelBulkJobHandler({ job_id: 42 })
63
+ expect(res.isError).toBe(true)
64
+ })
48
65
  })