@coldiq/mcp 0.1.0

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 (176) hide show
  1. package/dist/client.d.ts +8 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +47 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/executor.d.ts +21 -0
  6. package/dist/executor.d.ts.map +1 -0
  7. package/dist/executor.js +130 -0
  8. package/dist/executor.js.map +1 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/registry.d.ts +49 -0
  14. package/dist/registry.d.ts.map +1 -0
  15. package/dist/registry.js +3104 -0
  16. package/dist/registry.js.map +1 -0
  17. package/dist/tools/enrich-company.d.ts +22 -0
  18. package/dist/tools/enrich-company.d.ts.map +1 -0
  19. package/dist/tools/enrich-company.js +21 -0
  20. package/dist/tools/enrich-company.js.map +1 -0
  21. package/dist/tools/enrich-email.d.ts +24 -0
  22. package/dist/tools/enrich-email.d.ts.map +1 -0
  23. package/dist/tools/enrich-email.js +19 -0
  24. package/dist/tools/enrich-email.js.map +1 -0
  25. package/dist/tools/enrich-emails.d.ts +31 -0
  26. package/dist/tools/enrich-emails.d.ts.map +1 -0
  27. package/dist/tools/enrich-emails.js +146 -0
  28. package/dist/tools/enrich-emails.js.map +1 -0
  29. package/dist/tools/enrich-person.d.ts +26 -0
  30. package/dist/tools/enrich-person.d.ts.map +1 -0
  31. package/dist/tools/enrich-person.js +23 -0
  32. package/dist/tools/enrich-person.js.map +1 -0
  33. package/dist/tools/fetch-page-content.d.ts +22 -0
  34. package/dist/tools/fetch-page-content.d.ts.map +1 -0
  35. package/dist/tools/fetch-page-content.js +32 -0
  36. package/dist/tools/fetch-page-content.js.map +1 -0
  37. package/dist/tools/find-email.d.ts +24 -0
  38. package/dist/tools/find-email.d.ts.map +1 -0
  39. package/dist/tools/find-email.js +19 -0
  40. package/dist/tools/find-email.js.map +1 -0
  41. package/dist/tools/find-emails.d.ts +31 -0
  42. package/dist/tools/find-emails.d.ts.map +1 -0
  43. package/dist/tools/find-emails.js +146 -0
  44. package/dist/tools/find-emails.js.map +1 -0
  45. package/dist/tools/find-influencers.d.ts +29 -0
  46. package/dist/tools/find-influencers.d.ts.map +1 -0
  47. package/dist/tools/find-influencers.js +30 -0
  48. package/dist/tools/find-influencers.js.map +1 -0
  49. package/dist/tools/find-people.d.ts +26 -0
  50. package/dist/tools/find-people.d.ts.map +1 -0
  51. package/dist/tools/find-people.js +61 -0
  52. package/dist/tools/find-people.js.map +1 -0
  53. package/dist/tools/find-phone.d.ts +24 -0
  54. package/dist/tools/find-phone.d.ts.map +1 -0
  55. package/dist/tools/find-phone.js +48 -0
  56. package/dist/tools/find-phone.js.map +1 -0
  57. package/dist/tools/find-signals.d.ts +26 -0
  58. package/dist/tools/find-signals.d.ts.map +1 -0
  59. package/dist/tools/find-signals.js +82 -0
  60. package/dist/tools/find-signals.js.map +1 -0
  61. package/dist/tools/search-ads.d.ts +33 -0
  62. package/dist/tools/search-ads.d.ts.map +1 -0
  63. package/dist/tools/search-ads.js +33 -0
  64. package/dist/tools/search-ads.js.map +1 -0
  65. package/dist/tools/search-companies.d.ts +42 -0
  66. package/dist/tools/search-companies.d.ts.map +1 -0
  67. package/dist/tools/search-companies.js +37 -0
  68. package/dist/tools/search-companies.js.map +1 -0
  69. package/dist/tools/search-jobs.d.ts +51 -0
  70. package/dist/tools/search-jobs.d.ts.map +1 -0
  71. package/dist/tools/search-jobs.js +64 -0
  72. package/dist/tools/search-jobs.js.map +1 -0
  73. package/dist/tools/search-places.d.ts +47 -0
  74. package/dist/tools/search-places.d.ts.map +1 -0
  75. package/dist/tools/search-places.js +42 -0
  76. package/dist/tools/search-places.js.map +1 -0
  77. package/dist/tools/search-reddit.d.ts +27 -0
  78. package/dist/tools/search-reddit.d.ts.map +1 -0
  79. package/dist/tools/search-reddit.js +30 -0
  80. package/dist/tools/search-reddit.js.map +1 -0
  81. package/dist/tools/search-seo.d.ts +37 -0
  82. package/dist/tools/search-seo.d.ts.map +1 -0
  83. package/dist/tools/search-seo.js +49 -0
  84. package/dist/tools/search-seo.js.map +1 -0
  85. package/dist/tools/search-web.d.ts +23 -0
  86. package/dist/tools/search-web.d.ts.map +1 -0
  87. package/dist/tools/search-web.js +20 -0
  88. package/dist/tools/search-web.js.map +1 -0
  89. package/dist/tools/verify-email.d.ts +20 -0
  90. package/dist/tools/verify-email.d.ts.map +1 -0
  91. package/dist/tools/verify-email.js +15 -0
  92. package/dist/tools/verify-email.js.map +1 -0
  93. package/package.json +28 -0
  94. package/src/client.ts +60 -0
  95. package/src/executor.ts +182 -0
  96. package/src/index.ts +155 -0
  97. package/src/registry.ts +3159 -0
  98. package/src/tools/enrich-company.ts +25 -0
  99. package/src/tools/enrich-person.ts +27 -0
  100. package/src/tools/fetch-page-content.ts +36 -0
  101. package/src/tools/find-email.ts +23 -0
  102. package/src/tools/find-emails.ts +190 -0
  103. package/src/tools/find-influencers.ts +34 -0
  104. package/src/tools/find-people.ts +69 -0
  105. package/src/tools/find-phone.ts +53 -0
  106. package/src/tools/find-signals.ts +93 -0
  107. package/src/tools/search-ads.ts +44 -0
  108. package/src/tools/search-companies.ts +41 -0
  109. package/src/tools/search-jobs.ts +73 -0
  110. package/src/tools/search-places.ts +52 -0
  111. package/src/tools/search-reddit.ts +34 -0
  112. package/src/tools/search-seo.ts +59 -0
  113. package/src/tools/search-web.ts +24 -0
  114. package/src/tools/verify-email.ts +19 -0
  115. package/test-ads-live.ts +77 -0
  116. package/test-company-live.ts +91 -0
  117. package/test-email-live.ts +171 -0
  118. package/test-influencers-live.ts +66 -0
  119. package/test-jobs-live.ts +69 -0
  120. package/test-linkupapi-live.ts +137 -0
  121. package/test-phone-live.ts +41 -0
  122. package/test-places-live.ts +89 -0
  123. package/test-reddit-live.ts +66 -0
  124. package/test-search-live.ts +79 -0
  125. package/test-seo-live.ts +68 -0
  126. package/test-web-live.ts +67 -0
  127. package/tests/client.test.ts +90 -0
  128. package/tests/executor.test.ts +83 -0
  129. package/tests/gtm/01-icp-to-emails.test.ts +43 -0
  130. package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
  131. package/tests/gtm/03-icp-to-phones.test.ts +39 -0
  132. package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
  133. package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
  134. package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
  135. package/tests/gtm/07-places-to-content.test.ts +50 -0
  136. package/tests/gtm/08-domain-to-account.test.ts +44 -0
  137. package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
  138. package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
  139. package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
  140. package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
  141. package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
  142. package/tests/gtm/14-empty-handoff.test.ts +43 -0
  143. package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
  144. package/tests/gtm/README.md +59 -0
  145. package/tests/gtm/harness.ts +217 -0
  146. package/tests/gtm/tools-bridge.ts +232 -0
  147. package/tests/gtm-scenarios.md +32 -0
  148. package/tests/live/smoke-report.ts +255 -0
  149. package/tests/live/smoke.test.ts +134 -0
  150. package/tests/registry-enrich-person.test.ts +447 -0
  151. package/tests/registry-fetch-page-content.test.ts +90 -0
  152. package/tests/registry-find-people.test.ts +467 -0
  153. package/tests/registry-find-signals.test.ts +470 -0
  154. package/tests/registry-linkupapi.test.ts +331 -0
  155. package/tests/registry-search-companies.test.ts +188 -0
  156. package/tests/registry-search-jobs.test.ts +116 -0
  157. package/tests/registry.test.ts +2210 -0
  158. package/tests/tools/enrich-company.test.ts +92 -0
  159. package/tests/tools/enrich-email.test.ts +94 -0
  160. package/tests/tools/enrich-emails.test.ts +271 -0
  161. package/tests/tools/enrich-person.test.ts +140 -0
  162. package/tests/tools/fetch-page-content.test.ts +108 -0
  163. package/tests/tools/find-influencers.test.ts +91 -0
  164. package/tests/tools/find-people.test.ts +344 -0
  165. package/tests/tools/find-phone.test.ts +100 -0
  166. package/tests/tools/find-signals.test.ts +110 -0
  167. package/tests/tools/search-ads.test.ts +182 -0
  168. package/tests/tools/search-companies.test.ts +58 -0
  169. package/tests/tools/search-jobs.test.ts +210 -0
  170. package/tests/tools/search-places.test.ts +114 -0
  171. package/tests/tools/search-reddit.test.ts +125 -0
  172. package/tests/tools/search-seo.test.ts +183 -0
  173. package/tests/tools/search-web.test.ts +79 -0
  174. package/tests/tools/verify-email.test.ts +68 -0
  175. package/tsconfig.json +17 -0
  176. package/vitest.config.ts +7 -0
@@ -0,0 +1,32 @@
1
+ # GTM Scenario Matrix
2
+
3
+ Manual regression sheet — 15 realistic buyer/GTM prompts mapped to the expected MCP tool and key parameters.
4
+
5
+ **How to use**: For each scenario, verify that the tool description + schema alone would lead an agent to pick the right tool with the right params. Update affected rows whenever a tool's description or routing changes.
6
+
7
+ | # | Prompt | Tool | Key params | Notes |
8
+ |---|---|---|---|---|
9
+ | 1 | "Find Series-B SaaS companies in France with 50–200 employees" | `search_companies` | `keywords: ['SaaS']`, `countries: ['FR']`, `funding_stages: ['series_b']`, `min_employees: 50`, `max_employees: 200` | Firmographic + funding combo |
10
+ | 2 | "Get the CEO and CRO of ColdIQ and Folk" | `find_people` | `company_domains: ['coldiq.com','folk.app']`, `job_titles: ['CEO','CRO']` | Multi-company, multi-title |
11
+ | 3 | "What's Michel Lieben's email at ColdIQ?" | `find_email` | `first_name: 'Michel'`, `last_name: 'Lieben'`, `domain: 'coldiq.com'` | Name+domain → email (find, not enrich) |
12
+ | 4 | "Is michel@coldiq.com a real, deliverable address?" | `verify_email` | `email: 'michel@coldiq.com'` | Deliverability check |
13
+ | 5 | "Enrich this LinkedIn profile: linkedin.com/in/michel-lieben" | `enrich_person` | `linkedin_url: 'https://www.linkedin.com/in/michel-lieben'` | URL-based enrichment |
14
+ | 6 | "What companies recently raised Series A in Europe?" | `find_signals` | `signal_type: 'funding'`, `countries: ['GB','FR','DE','NL']` | Funding signal, country-targeted |
15
+ | 7 | "Companies hiring SDRs in NYC right now" | `search_jobs` | `title_keywords: ['SDR']`, `locations: ['New York, United States']` | Live job postings (not `find_signals`) |
16
+ | 8 | "Show companies showing buying intent for Salesforce alternatives" | `find_signals` | `signal_type: 'intent'`, `companies: ['Salesforce']` or `domains: ['salesforce.com']` | Intent requires companies or domains |
17
+ | 9 | "What's the Google SEO ranking for 'cold email tool'?" | `search_seo` | `category: 'serp'`, `keyword: 'cold email tool'`, `engine: 'google'` | SERP category |
18
+ | 10 | "What tech stack does stripe.com use?" | `search_seo` | `category: 'domain'`, `target: 'stripe.com'`, `action: 'technologies'` | Domain-analytics category |
19
+ | 11 | "Find B2B SaaS influencers on LinkedIn" | `find_influencers` | `platform: 'linkedin'`, `ai_search: 'B2B SaaS founders'` | Discovery mode (no handle) |
20
+ | 12 | "What does the ColdIQ homepage say?" | `fetch_page_content` | `urls: ['https://coldiq.com']` | Page text extraction (find URL via `search_web` first if unknown) |
21
+ | 13 | "Coffee shops near Times Square" | `search_places` | `query: 'coffee shop'`, `city: 'New York'`, `country: 'US'` | Local place search |
22
+ | 14 | "Reddit threads about cold email tools" | `search_reddit` | `query: 'cold email tool'`, `sort: 'top'` | One async provider |
23
+ | 15 | "LinkedIn ads run by ColdIQ" | `search_ads` | `platform: 'linkedin'`, `search_urls: ['...']` | Requires pre-built LinkedIn Ad Library URL |
24
+
25
+ ## Boundary pairs (easily confused tools)
26
+
27
+ | A | B | Distinction |
28
+ |---|---|---|
29
+ | `search_jobs` | `find_signals(hiring)` | `search_jobs` = individual live postings; `find_signals(hiring)` = aggregated hiring-surge signal at company level |
30
+ | `find_email` | `enrich_person` | `find_email` = name+domain → email; `enrich_person` = any identifier → full profile |
31
+ | `fetch_page_content` | `search_web` | `search_web` = URL discovery; `fetch_page_content` = fetch full text of known URLs |
32
+ | `find_signals(news)` | `find_signals(intent)` | `news`/`startup_post` are feed-style (country only, no company filter); `intent` is company-targeted |
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Live smoke + waterfall report.
3
+ *
4
+ * Runs every MCP tool with minimal-credit payloads, captures the executor's
5
+ * waterfall logs, and prints a structured report at the end.
6
+ *
7
+ * Usage:
8
+ * COLDIQ_API_KEY=<key> npx tsx mcp/tests/live/smoke-report.ts
9
+ *
10
+ * Env:
11
+ * COLDIQ_API_KEY required
12
+ * COLDIQ_API_URL optional (default: https://api.coldiq.com)
13
+ */
14
+
15
+ process.env.COLDIQ_DEBUG = '1' // captures `skipping <id>` debug lines
16
+
17
+ import { initClient } from '../../src/client.js'
18
+ import { enrichCompanyHandler } from '../../src/tools/enrich-company.js'
19
+ import { enrichPersonHandler } from '../../src/tools/enrich-person.js'
20
+ import { fetchPageContentHandler } from '../../src/tools/fetch-page-content.js'
21
+ import { findEmailHandler } from '../../src/tools/find-email.js'
22
+ import { findEmailsHandler } from '../../src/tools/find-emails.js'
23
+ import { findInfluencersHandler } from '../../src/tools/find-influencers.js'
24
+ import { findPeopleHandler } from '../../src/tools/find-people.js'
25
+ import { findPhoneHandler } from '../../src/tools/find-phone.js'
26
+ import { findSignalsHandler } from '../../src/tools/find-signals.js'
27
+ import { searchAdsHandler } from '../../src/tools/search-ads.js'
28
+ import { searchCompaniesHandler } from '../../src/tools/search-companies.js'
29
+ import { searchJobsHandler } from '../../src/tools/search-jobs.js'
30
+ import { searchPlacesHandler } from '../../src/tools/search-places.js'
31
+ import { searchRedditHandler } from '../../src/tools/search-reddit.js'
32
+ import { searchSeoHandler } from '../../src/tools/search-seo.js'
33
+ import { searchWebHandler } from '../../src/tools/search-web.js'
34
+ import { verifyEmailHandler } from '../../src/tools/verify-email.js'
35
+
36
+ type Handler = (input: Record<string, unknown>) => Promise<{ content: { text: string }[]; isError?: boolean }>
37
+
38
+ interface Case {
39
+ tool: string
40
+ variant: string
41
+ handler: Handler
42
+ input: Record<string, unknown>
43
+ }
44
+
45
+ interface CaseResult {
46
+ tool: string
47
+ variant: string
48
+ ok: boolean
49
+ winner: string | null
50
+ latencyMs: number | null
51
+ tried: string[] // providers that fired (excluding the winner)
52
+ failed: { id: string; reason: string }[]
53
+ skipped: string[] // providers gated out
54
+ errorMsg?: string
55
+ totalDurationMs: number
56
+ }
57
+
58
+ const API_URL = process.env.COLDIQ_API_URL ?? 'https://api.coldiq.com'
59
+ const API_KEY = process.env.COLDIQ_API_KEY
60
+
61
+ if (!API_KEY) {
62
+ console.error('COLDIQ_API_KEY is required')
63
+ process.exit(1)
64
+ }
65
+
66
+ initClient(API_URL, API_KEY)
67
+
68
+ // ─── Cases ─────────────────────────────────────────────────────────────────
69
+
70
+ const cases: Case[] = [
71
+ // 1 case each (sync, cheap)
72
+ { tool: 'verify_email', variant: 'michel@coldiq.com', handler: verifyEmailHandler, input: { email: 'michel@coldiq.com' } },
73
+ { tool: 'find_email', variant: 'Michel Lieben at coldiq', handler: findEmailHandler, input: { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' } },
74
+ { tool: 'find_emails', variant: 'bulk single contact', handler: findEmailsHandler, input: { people: [{ id: 'michel-lieben', first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' }] } },
75
+ { tool: 'find_phone', variant: 'Brahim LinkedIn', handler: findPhoneHandler, input: { linkedin_url: 'https://www.linkedin.com/in/brahim-sliti/' } },
76
+ { tool: 'enrich_company', variant: 'coldiq.com', handler: enrichCompanyHandler, input: { domain: 'coldiq.com' } },
77
+ { tool: 'find_people', variant: 'CEO at ColdIQ', handler: findPeopleHandler, input: { company_domains: ['coldiq.com'], job_titles: ['CEO'], limit: 1 } },
78
+ { tool: 'search_web', variant: 'ColdIQ outbound', handler: searchWebHandler, input: { query: 'ColdIQ outbound sales', limit: 1 } },
79
+ { tool: 'fetch_page_content', variant: 'coldiq.com', handler: fetchPageContentHandler, input: { urls: ['https://coldiq.com'] } },
80
+ { tool: 'find_signals', variant: 'funding in FR', handler: findSignalsHandler, input: { signal_type: 'funding', countries: ['FR'], limit: 1 } },
81
+ { tool: 'search_seo', variant: 'serp google: coldiq', handler: searchSeoHandler, input: { category: 'serp', keyword: 'coldiq', limit: 1 } },
82
+ { tool: 'find_influencers', variant: 'Instagram discovery', handler: findInfluencersHandler, input: { platform: 'instagram', ai_search: 'B2B SaaS founders', limit: 1 } },
83
+
84
+ // tools with branching gates — 2 variants
85
+ { tool: 'enrich_person', variant: 'by email', handler: enrichPersonHandler, input: { email: 'michel@coldiq.com' } },
86
+ { tool: 'enrich_person', variant: 'by linkedin_url', handler: enrichPersonHandler, input: { linkedin_url: 'https://www.linkedin.com/in/michel-lieben' } },
87
+
88
+ { tool: 'search_companies', variant: 'keyword + country', handler: searchCompaniesHandler, input: { keywords: ['SaaS'], countries: ['FR'], limit: 1 } },
89
+ { tool: 'search_companies', variant: 'tech-stack filter', handler: searchCompaniesHandler, input: { technologies: ['Salesforce'], limit: 1 } },
90
+
91
+ { tool: 'search_places', variant: 'US (Openmart route)', handler: searchPlacesHandler, input: { query: 'coffee shop', country: 'US', limit: 1 } },
92
+ { tool: 'search_places', variant: 'FR (Google Maps route)', handler: searchPlacesHandler, input: { query: 'café', country: 'FR', limit: 1 } },
93
+
94
+ // async tools — 1 each (slow)
95
+ { tool: 'search_jobs', variant: 'SDR in Paris', handler: searchJobsHandler, input: { title_keywords: ['SDR'], locations: ['Paris'], limit: 10 } },
96
+ { tool: 'search_ads', variant: 'meta: ColdIQ', handler: searchAdsHandler, input: { platform: 'meta', query: 'ColdIQ' } },
97
+ { tool: 'search_reddit', variant: 'r/sales top posts', handler: searchRedditHandler, input: { start_urls: ['https://www.reddit.com/r/sales/'], sort: 'hot', limit: 3 } },
98
+ ]
99
+
100
+ // ─── Log capture ───────────────────────────────────────────────────────────
101
+
102
+ const reLog = /^\[coldiq\] (\S+) → (.+)$/
103
+ const reDebug = /^\[coldiq:debug\] (\S+) → (.+)$/
104
+
105
+ function captureCase(): { stop: () => string[]; lines: string[] } {
106
+ const lines: string[] = []
107
+ const orig = console.error
108
+ console.error = (...args: unknown[]) => {
109
+ const line = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')
110
+ lines.push(line)
111
+ }
112
+ return {
113
+ lines,
114
+ stop: () => { console.error = orig; return lines },
115
+ }
116
+ }
117
+
118
+ function parseLogs(lines: string[]): Pick<CaseResult, 'winner' | 'latencyMs' | 'tried' | 'failed' | 'skipped'> {
119
+ let winner: string | null = null
120
+ let latencyMs: number | null = null
121
+ const tried: string[] = []
122
+ const failed: { id: string; reason: string }[] = []
123
+ const skipped: string[] = []
124
+
125
+ for (const raw of lines) {
126
+ const dm = raw.match(reDebug)
127
+ if (dm) {
128
+ const m = dm[2].match(/^skipping (\S+) /)
129
+ if (m) skipped.push(m[1])
130
+ continue
131
+ }
132
+ const lm = raw.match(reLog)
133
+ if (!lm) continue
134
+ const rest = lm[2]
135
+ const tMatch = rest.match(/^trying (\S+)$/)
136
+ const wMatch = rest.match(/^(\S+) ✓ \((\d+)ms\)$/)
137
+ const fMatch = rest.match(/^(\S+) ✗ (.+)$/)
138
+ if (tMatch) { tried.push(tMatch[1]); continue }
139
+ if (wMatch) { winner = wMatch[1]; latencyMs = parseInt(wMatch[2], 10); continue }
140
+ if (fMatch) { failed.push({ id: fMatch[1], reason: fMatch[2] }); continue }
141
+ }
142
+ // Remove the winner from tried list (it'll be in there from the "trying" log)
143
+ const triedExcludingWinner = tried.filter((t) => t !== winner)
144
+ return { winner, latencyMs, tried: triedExcludingWinner, failed, skipped }
145
+ }
146
+
147
+ // ─── Run ───────────────────────────────────────────────────────────────────
148
+
149
+ async function runCase(c: Case): Promise<CaseResult> {
150
+ const cap = captureCase()
151
+ const start = Date.now()
152
+ let ok = false
153
+ let errorMsg: string | undefined
154
+ try {
155
+ const r = await c.handler(c.input)
156
+ ok = !r.isError
157
+ if (!ok) {
158
+ try {
159
+ const parsed = JSON.parse(r.content[0].text)
160
+ errorMsg = typeof parsed.error === 'string' ? parsed.error : JSON.stringify(parsed).slice(0, 200)
161
+ } catch {
162
+ errorMsg = r.content[0].text.slice(0, 200)
163
+ }
164
+ }
165
+ } catch (e) {
166
+ ok = false
167
+ errorMsg = e instanceof Error ? e.message : String(e)
168
+ }
169
+ const totalDurationMs = Date.now() - start
170
+ cap.stop()
171
+ const parsed = parseLogs(cap.lines)
172
+ return { tool: c.tool, variant: c.variant, ok, totalDurationMs, errorMsg, ...parsed }
173
+ }
174
+
175
+ function fmtList(xs: string[]): string {
176
+ return xs.length === 0 ? '—' : xs.join(', ')
177
+ }
178
+
179
+ function printCase(r: CaseResult, idx: number, total: number): void {
180
+ const status = r.ok ? '✅' : '❌'
181
+ const winner = r.winner ?? '—'
182
+ const lat = r.latencyMs !== null ? `${r.latencyMs}ms` : `${r.totalDurationMs}ms total`
183
+ process.stdout.write(`[${idx + 1}/${total}] ${status} ${r.tool} (${r.variant}) — ${winner} (${lat})\n`)
184
+ if (!r.ok && r.errorMsg) {
185
+ process.stdout.write(` error: ${r.errorMsg.slice(0, 200)}\n`)
186
+ }
187
+ }
188
+
189
+ function printReport(results: CaseResult[]): void {
190
+ const groups = new Map<string, CaseResult[]>()
191
+ for (const r of results) {
192
+ const arr = groups.get(r.tool) ?? []
193
+ arr.push(r)
194
+ groups.set(r.tool, arr)
195
+ }
196
+
197
+ // Final markdown report
198
+ console.log('\n')
199
+ console.log('# MCP Live Smoke Report')
200
+ console.log('')
201
+ console.log(`Run at: ${new Date().toISOString()}`)
202
+ console.log(`API: ${API_URL}`)
203
+ console.log('')
204
+
205
+ console.log('## Summary')
206
+ console.log('')
207
+ console.log('| Tool | Cases | Status | Winners | Total Latency |')
208
+ console.log('|---|---|---|---|---|')
209
+ for (const [tool, rs] of groups) {
210
+ const allOk = rs.every((r) => r.ok)
211
+ const status = allOk ? '✅' : rs.every((r) => !r.ok) ? '❌' : '⚠️'
212
+ const winners = [...new Set(rs.map((r) => r.winner ?? '—'))].join(' / ')
213
+ const totalMs = rs.reduce((s, r) => s + r.totalDurationMs, 0)
214
+ console.log(`| ${tool} | ${rs.length} | ${status} | ${winners} | ${totalMs}ms |`)
215
+ }
216
+ const okCount = results.filter((r) => r.ok).length
217
+ console.log('')
218
+ console.log(`**${okCount}/${results.length} cases passed**`)
219
+ console.log('')
220
+
221
+ console.log('## Per-tool detail')
222
+ console.log('')
223
+ for (const [tool, rs] of groups) {
224
+ console.log(`### ${tool}`)
225
+ console.log('')
226
+ console.log('| Variant | Status | Winner | Latency | Skipped (gated) | Tried before winner | Failed |')
227
+ console.log('|---|---|---|---|---|---|---|')
228
+ for (const r of rs) {
229
+ const status = r.ok ? '✅' : '❌'
230
+ const winner = r.winner ?? '—'
231
+ const lat = r.latencyMs !== null ? `${r.latencyMs}ms` : `(${r.totalDurationMs}ms total)`
232
+ const skipped = fmtList(r.skipped)
233
+ const triedBefore = fmtList(r.tried)
234
+ const failed = r.failed.length === 0 ? '—' : r.failed.map((f) => `${f.id}: ${f.reason.slice(0, 40)}`).join('; ')
235
+ console.log(`| ${r.variant} | ${status} | ${winner} | ${lat} | ${skipped} | ${triedBefore} | ${failed} |`)
236
+ }
237
+ console.log('')
238
+ }
239
+ }
240
+
241
+ async function main(): Promise<void> {
242
+ process.stdout.write(`Running ${cases.length} live cases against ${API_URL}\n\n`)
243
+ const results: CaseResult[] = []
244
+ for (let i = 0; i < cases.length; i++) {
245
+ const r = await runCase(cases[i])
246
+ results.push(r)
247
+ printCase(r, i, cases.length)
248
+ }
249
+ printReport(results)
250
+ }
251
+
252
+ main().catch((e) => {
253
+ console.error('FATAL:', e)
254
+ process.exit(1)
255
+ })
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Live smoke tests — require real API access.
3
+ *
4
+ * Run with:
5
+ * LIVE_TESTS=1 COLDIQ_API_KEY=<key> npm test
6
+ * or with a custom base URL:
7
+ * LIVE_TESTS=1 COLDIQ_API_URL=https://api.coldiq.com COLDIQ_API_KEY=<key> npm test
8
+ *
9
+ * Expected credit cost per full run: ~17–20 credits (limit:1 everywhere).
10
+ * Assertions are lenient — provider availability varies day-to-day.
11
+ */
12
+
13
+ import { describe, it, expect, beforeAll } from 'vitest'
14
+ import { initClient } from '../../src/client.js'
15
+ import { searchCompaniesHandler } from '../../src/tools/search-companies.js'
16
+ import { findPeopleHandler } from '../../src/tools/find-people.js'
17
+ import { findEmailHandler } from '../../src/tools/find-email.js'
18
+ import { findEmailsHandler } from '../../src/tools/find-emails.js'
19
+ import { verifyEmailHandler } from '../../src/tools/verify-email.js'
20
+ import { findPhoneHandler } from '../../src/tools/find-phone.js'
21
+ import { enrichCompanyHandler } from '../../src/tools/enrich-company.js'
22
+ import { enrichPersonHandler } from '../../src/tools/enrich-person.js'
23
+ import { searchWebHandler } from '../../src/tools/search-web.js'
24
+ import { searchJobsHandler } from '../../src/tools/search-jobs.js'
25
+ import { searchAdsHandler } from '../../src/tools/search-ads.js'
26
+ import { searchPlacesHandler } from '../../src/tools/search-places.js'
27
+ import { findInfluencersHandler } from '../../src/tools/find-influencers.js'
28
+ import { searchRedditHandler } from '../../src/tools/search-reddit.js'
29
+ import { searchSeoHandler } from '../../src/tools/search-seo.js'
30
+ import { findSignalsHandler } from '../../src/tools/find-signals.js'
31
+ import { fetchPageContentHandler } from '../../src/tools/fetch-page-content.js'
32
+
33
+ const RUN_LIVE = process.env.LIVE_TESTS === '1'
34
+
35
+ function assertSuccess(result: { content: { text: string }[]; isError?: boolean }, label: string) {
36
+ expect(result.isError, `${label}: isError`).toBeFalsy()
37
+ const parsed = JSON.parse(result.content[0].text)
38
+ expect(parsed._meta?.provider, `${label}: _meta.provider`).toBeTruthy()
39
+ return parsed
40
+ }
41
+
42
+ describe.runIf(RUN_LIVE)('live smoke', () => {
43
+ beforeAll(() => {
44
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
45
+ })
46
+
47
+ it('search_companies — SaaS in FR', async () => {
48
+ const result = await searchCompaniesHandler({ keywords: ['SaaS'], countries: ['FR'], limit: 1 })
49
+ assertSuccess(result, 'search_companies')
50
+ }, 60_000)
51
+
52
+ it('find_people — CEO at ColdIQ', async () => {
53
+ const result = await findPeopleHandler({ company_domains: ['coldiq.com'], job_titles: ['CEO'], limit: 1 })
54
+ const parsed = assertSuccess(result, 'find_people')
55
+ expect(parsed.data).toBeTruthy()
56
+ }, 60_000)
57
+
58
+ it('find_email — Michel Lieben at coldiq.com', async () => {
59
+ const result = await findEmailHandler({ first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })
60
+ assertSuccess(result, 'find_email')
61
+ }, 60_000)
62
+
63
+ it('find_emails — bulk: Michel Lieben at coldiq.com', async () => {
64
+ const result = await findEmailsHandler({ people: [{ id: 'michel-lieben', first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' }] })
65
+ assertSuccess(result, 'find_emails')
66
+ }, 60_000)
67
+
68
+ it('verify_email — michel@coldiq.com', async () => {
69
+ const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
70
+ assertSuccess(result, 'verify_email')
71
+ }, 60_000)
72
+
73
+ it('find_phone — Michel Lieben LinkedIn', async () => {
74
+ const result = await findPhoneHandler({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })
75
+ // find_phone may return no result if number is private — just check no crash
76
+ expect(result.content[0].text).toBeTruthy()
77
+ }, 60_000)
78
+
79
+ it('enrich_company — coldiq.com', async () => {
80
+ const result = await enrichCompanyHandler({ domain: 'coldiq.com' })
81
+ assertSuccess(result, 'enrich_company')
82
+ }, 60_000)
83
+
84
+ it('enrich_person — michel@coldiq.com', async () => {
85
+ const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
86
+ assertSuccess(result, 'enrich_person')
87
+ }, 60_000)
88
+
89
+ it('search_web — ColdIQ outbound', async () => {
90
+ const result = await searchWebHandler({ query: 'ColdIQ outbound', limit: 1 })
91
+ assertSuccess(result, 'search_web')
92
+ }, 60_000)
93
+
94
+ it('search_jobs — SDR in Paris', async () => {
95
+ const result = await searchJobsHandler({ title_keywords: ['SDR'], locations: ['Paris, France'], limit: 10 })
96
+ assertSuccess(result, 'search_jobs')
97
+ }, 120_000)
98
+
99
+ it('search_ads — Meta ads for ColdIQ', async () => {
100
+ const result = await searchAdsHandler({ platform: 'meta', query: 'ColdIQ' })
101
+ // ads may return empty for small advertisers — just check no crash
102
+ expect(result.content[0].text).toBeTruthy()
103
+ }, 120_000)
104
+
105
+ it('search_places — coffee shop in Paris', async () => {
106
+ const result = await searchPlacesHandler({ query: 'coffee shop', country: 'FR', limit: 1 })
107
+ assertSuccess(result, 'search_places')
108
+ }, 120_000)
109
+
110
+ it('find_influencers — Instagram B2B SaaS', async () => {
111
+ const result = await findInfluencersHandler({ platform: 'instagram', ai_search: 'B2B SaaS founders', limit: 1 })
112
+ assertSuccess(result, 'find_influencers')
113
+ }, 60_000)
114
+
115
+ it('search_reddit — outbound sales posts', async () => {
116
+ const result = await searchRedditHandler({ query: 'outbound sales', limit: 1 })
117
+ assertSuccess(result, 'search_reddit')
118
+ }, 120_000)
119
+
120
+ it('search_seo — SERP for coldiq', async () => {
121
+ const result = await searchSeoHandler({ category: 'serp', keyword: 'coldiq', limit: 1 })
122
+ assertSuccess(result, 'search_seo')
123
+ }, 60_000)
124
+
125
+ it('find_signals — funding in FR', async () => {
126
+ const result = await findSignalsHandler({ signal_type: 'funding', countries: ['FR'], limit: 1 })
127
+ assertSuccess(result, 'find_signals')
128
+ }, 60_000)
129
+
130
+ it('fetch_page_content — coldiq.com homepage', async () => {
131
+ const result = await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
132
+ assertSuccess(result, 'fetch_page_content')
133
+ }, 60_000)
134
+ })