@coldiq/mcp 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/enrich-company.d.ts +3 -9
- package/dist/tools/enrich-company.d.ts.map +1 -1
- package/dist/tools/enrich-company.js +3 -16
- package/dist/tools/enrich-company.js.map +1 -1
- package/dist/tools/enrich-person.d.ts +3 -9
- package/dist/tools/enrich-person.d.ts.map +1 -1
- package/dist/tools/enrich-person.js +3 -12
- package/dist/tools/enrich-person.js.map +1 -1
- package/dist/tools/fetch-page-content.d.ts +3 -9
- package/dist/tools/fetch-page-content.d.ts.map +1 -1
- package/dist/tools/fetch-page-content.js +3 -12
- package/dist/tools/fetch-page-content.js.map +1 -1
- package/dist/tools/find-email.d.ts +3 -9
- package/dist/tools/find-email.d.ts.map +1 -1
- package/dist/tools/find-email.js +3 -40
- package/dist/tools/find-email.js.map +1 -1
- package/dist/tools/find-emails.d.ts +3 -4
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +8 -423
- package/dist/tools/find-emails.js.map +1 -1
- package/dist/tools/find-influencers.d.ts +3 -9
- package/dist/tools/find-influencers.d.ts.map +1 -1
- package/dist/tools/find-influencers.js +3 -12
- package/dist/tools/find-influencers.js.map +1 -1
- package/dist/tools/find-people.d.ts +3 -8
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +3 -98
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/find-phone.d.ts +3 -9
- package/dist/tools/find-phone.d.ts.map +1 -1
- package/dist/tools/find-phone.js +4 -41
- package/dist/tools/find-phone.js.map +1 -1
- package/dist/tools/find-signals.d.ts +3 -9
- package/dist/tools/find-signals.d.ts.map +1 -1
- package/dist/tools/find-signals.js +3 -83
- package/dist/tools/find-signals.js.map +1 -1
- package/dist/tools/get-place-reviews.d.ts +3 -9
- package/dist/tools/get-place-reviews.d.ts.map +1 -1
- package/dist/tools/get-place-reviews.js +3 -12
- package/dist/tools/get-place-reviews.js.map +1 -1
- package/dist/tools/search-ads.d.ts +3 -9
- package/dist/tools/search-ads.d.ts.map +1 -1
- package/dist/tools/search-ads.js +3 -12
- package/dist/tools/search-ads.js.map +1 -1
- package/dist/tools/search-companies.d.ts +3 -9
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +3 -24
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/tools/search-jobs.d.ts +3 -9
- package/dist/tools/search-jobs.d.ts.map +1 -1
- package/dist/tools/search-jobs.js +3 -27
- package/dist/tools/search-jobs.js.map +1 -1
- package/dist/tools/search-places.d.ts +3 -15
- package/dist/tools/search-places.d.ts.map +1 -1
- package/dist/tools/search-places.js +5 -76
- package/dist/tools/search-places.js.map +1 -1
- package/dist/tools/search-reddit.d.ts +3 -9
- package/dist/tools/search-reddit.d.ts.map +1 -1
- package/dist/tools/search-reddit.js +3 -12
- package/dist/tools/search-reddit.js.map +1 -1
- package/dist/tools/search-seo.d.ts +3 -9
- package/dist/tools/search-seo.d.ts.map +1 -1
- package/dist/tools/search-seo.js +3 -12
- package/dist/tools/search-seo.js.map +1 -1
- package/dist/tools/search-web.d.ts +3 -9
- package/dist/tools/search-web.d.ts.map +1 -1
- package/dist/tools/search-web.js +3 -12
- package/dist/tools/search-web.js.map +1 -1
- package/dist/tools/verify-email.d.ts +3 -10
- package/dist/tools/verify-email.d.ts.map +1 -1
- package/dist/tools/verify-email.js +3 -68
- package/dist/tools/verify-email.js.map +1 -1
- package/dist/verb-client.d.ts +35 -0
- package/dist/verb-client.d.ts.map +1 -0
- package/dist/verb-client.js +57 -0
- package/dist/verb-client.js.map +1 -0
- package/package.json +1 -1
- package/src/tools/enrich-company.ts +3 -16
- package/src/tools/enrich-person.ts +3 -12
- package/src/tools/fetch-page-content.ts +3 -12
- package/src/tools/find-email.ts +3 -42
- package/src/tools/find-emails.ts +8 -506
- package/src/tools/find-influencers.ts +3 -12
- package/src/tools/find-people.ts +3 -107
- package/src/tools/find-phone.ts +4 -42
- package/src/tools/find-signals.ts +3 -88
- package/src/tools/get-place-reviews.ts +3 -12
- package/src/tools/search-ads.ts +3 -12
- package/src/tools/search-companies.ts +3 -23
- package/src/tools/search-jobs.ts +3 -30
- package/src/tools/search-places.ts +5 -72
- package/src/tools/search-reddit.ts +3 -12
- package/src/tools/search-seo.ts +3 -12
- package/src/tools/search-web.ts +3 -12
- package/src/tools/verify-email.ts +3 -76
- package/src/verb-client.ts +93 -0
- package/tests/verb-client.test.ts +152 -0
- package/tests/tools/enrich-company.test.ts +0 -92
- package/tests/tools/enrich-person.test.ts +0 -140
- package/tests/tools/fetch-page-content.test.ts +0 -108
- package/tests/tools/find-email.test.ts +0 -137
- package/tests/tools/find-emails.test.ts +0 -818
- package/tests/tools/find-influencers.test.ts +0 -91
- package/tests/tools/find-people.test.ts +0 -621
- package/tests/tools/find-phone.test.ts +0 -100
- package/tests/tools/find-signals.test.ts +0 -175
- package/tests/tools/get-place-reviews.test.ts +0 -73
- package/tests/tools/search-ads.test.ts +0 -182
- package/tests/tools/search-companies.test.ts +0 -192
- package/tests/tools/search-jobs.test.ts +0 -210
- package/tests/tools/search-places.test.ts +0 -210
- package/tests/tools/search-reddit.test.ts +0 -194
- package/tests/tools/search-seo.test.ts +0 -183
- package/tests/tools/search-web.test.ts +0 -79
- package/tests/tools/verify-email.test.ts +0 -142
package/src/tools/find-phone.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const findPhoneName = 'find_phone'
|
|
6
6
|
|
|
@@ -16,45 +16,7 @@ export const findPhoneSchema = {
|
|
|
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
17
|
}
|
|
18
18
|
|
|
19
|
-
const inputSchema = z
|
|
20
|
-
.object({
|
|
21
|
-
linkedin_url: z.string().url().optional(),
|
|
22
|
-
first_name: z.string().optional(),
|
|
23
|
-
last_name: z.string().optional(),
|
|
24
|
-
company_domain: z.string().optional(),
|
|
25
|
-
company_name: z.string().optional(),
|
|
26
|
-
})
|
|
27
|
-
.superRefine((val, ctx) => {
|
|
28
|
-
const hasLinkedIn = typeof val.linkedin_url === 'string' && val.linkedin_url.length > 0
|
|
29
|
-
const hasName = typeof val.first_name === 'string' && val.first_name.length > 0 &&
|
|
30
|
-
typeof val.last_name === 'string' && val.last_name.length > 0
|
|
31
|
-
const hasCompany = (typeof val.company_domain === 'string' && val.company_domain.length > 0) ||
|
|
32
|
-
(typeof val.company_name === 'string' && val.company_name.length > 0)
|
|
33
|
-
if (!hasLinkedIn && !(hasName && hasCompany)) {
|
|
34
|
-
ctx.addIssue({
|
|
35
|
-
code: 'custom',
|
|
36
|
-
message: 'Provide either linkedin_url or both first_name+last_name and company_domain/company_name',
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
|
|
41
19
|
export async function findPhoneHandler(input: Record<string, unknown>) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!resolved.ok) {
|
|
45
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
46
|
-
}
|
|
47
|
-
const validation = inputSchema.safeParse(restInput)
|
|
48
|
-
if (!validation.success) {
|
|
49
|
-
const msg = validation.error.issues.map((i) => i.message).join('; ')
|
|
50
|
-
return {
|
|
51
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }) }],
|
|
52
|
-
isError: true,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const result = await executeWithFallback('find_phone', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
56
|
-
if (isExecutionError(result)) {
|
|
57
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
58
|
-
}
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
20
|
+
// MCP schema predates the verb: `company_domain` → the verb's `domain`.
|
|
21
|
+
return callVerb('/phone/find', input, { remap: { company_domain: 'domain' } })
|
|
60
22
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const findSignalsName = 'find_signals'
|
|
6
6
|
|
|
@@ -61,90 +61,5 @@ export const findSignalsSchema = {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export async function findSignalsHandler(input: Record<string, unknown>) {
|
|
64
|
-
|
|
65
|
-
const hasCompanies = Array.isArray(restInput.companies) && (restInput.companies as unknown[]).length > 0
|
|
66
|
-
const hasDomains = Array.isArray(restInput.domains) && (restInput.domains as unknown[]).length > 0
|
|
67
|
-
|
|
68
|
-
const hasTopics = Array.isArray(restInput.topics) && (restInput.topics as unknown[]).length > 0
|
|
69
|
-
|
|
70
|
-
if (restInput.signal_type === 'intent' && !hasCompanies && !hasDomains && !hasTopics) {
|
|
71
|
-
return {
|
|
72
|
-
content: [{
|
|
73
|
-
type: 'text' as const,
|
|
74
|
-
text: JSON.stringify({ error: 'intent signal_type requires at least one of: topics (to discover companies by intent topic), or companies/domains (to verify intent on known companies)' }),
|
|
75
|
-
}],
|
|
76
|
-
isError: true,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if ((restInput.signal_type === 'news' || restInput.signal_type === 'startup_post') && (hasCompanies || hasDomains)) {
|
|
81
|
-
return {
|
|
82
|
-
content: [{
|
|
83
|
-
type: 'text' as const,
|
|
84
|
-
text: JSON.stringify({
|
|
85
|
-
error: 'news and startup_post are feed-style signal types that only filter by country and since date — they do not support company filtering. Remove companies/domains, or use signal_type=intent for company-targeted intent signals.',
|
|
86
|
-
}),
|
|
87
|
-
}],
|
|
88
|
-
isError: true,
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const resolved = resolvePreferredProviders('find_signals', restInput, rawUseProviders)
|
|
93
|
-
if (!resolved.ok) {
|
|
94
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const result = await executeWithFallback('find_signals', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
98
|
-
if (isExecutionError(result)) {
|
|
99
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// buying_intents upstream has no limit param — truncate to requested limit
|
|
103
|
-
if (restInput.signal_type === 'intent') {
|
|
104
|
-
const limit = typeof restInput.limit === 'number' ? restInput.limit : 25
|
|
105
|
-
const typed = result as { data?: { data?: unknown[] } }
|
|
106
|
-
if (Array.isArray(typed.data?.data)) {
|
|
107
|
-
typed.data!.data = typed.data!.data.slice(0, limit)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Signalbase /hiring-signals does not accept an industry filter upstream, so the
|
|
112
|
-
// `industries` param would otherwise be silently dropped. Filter client-side:
|
|
113
|
-
// each hiring row carries an `industries` string (e.g. "Law Practice and Legal
|
|
114
|
-
// Services") which we substring-match against the user-supplied list.
|
|
115
|
-
//
|
|
116
|
-
// Non-destructive fallback: Signalbase has no industry facet and tags rows with
|
|
117
|
-
// coarse labels (e.g. "Financial Services"), so a user term like "Fintech" can
|
|
118
|
-
// match nothing even when relevant rows exist. Rather than return a misleading
|
|
119
|
-
// empty set (which reads as "no companies are hiring"), when the filter would
|
|
120
|
-
// drop every row we keep the unfiltered rows and attach a note explaining that
|
|
121
|
-
// the industry filter matched nothing.
|
|
122
|
-
if (restInput.signal_type === 'hiring' && Array.isArray(restInput.industries) && restInput.industries.length > 0) {
|
|
123
|
-
const wanted = (restInput.industries as unknown[])
|
|
124
|
-
.map((s) => (typeof s === 'string' ? s.toLowerCase() : ''))
|
|
125
|
-
.filter((s) => s.length > 0)
|
|
126
|
-
if (wanted.length > 0) {
|
|
127
|
-
const typed = result as { data?: { data?: unknown[]; _industry_filter?: string } }
|
|
128
|
-
const rows = typed.data?.data
|
|
129
|
-
if (Array.isArray(rows) && rows.length > 0) {
|
|
130
|
-
const filtered = rows.filter((row) => {
|
|
131
|
-
if (!row || typeof row !== 'object') return false
|
|
132
|
-
const industriesField = (row as Record<string, unknown>).industries
|
|
133
|
-
if (typeof industriesField !== 'string' || industriesField.length === 0) return false
|
|
134
|
-
const haystack = industriesField.toLowerCase()
|
|
135
|
-
return wanted.some((needle) => haystack.includes(needle))
|
|
136
|
-
})
|
|
137
|
-
if (filtered.length > 0) {
|
|
138
|
-
typed.data!.data = filtered
|
|
139
|
-
} else {
|
|
140
|
-
typed.data!._industry_filter =
|
|
141
|
-
`No hiring rows matched industries [${(restInput.industries as string[]).join(', ')}]. ` +
|
|
142
|
-
'Signalbase tags hiring rows with coarse industry labels (e.g. "Financial Services"), so a narrow term may match nothing — results are returned UNFILTERED. ' +
|
|
143
|
-
'Narrow with countries or a broader/more exact industry label (e.g. "Financial Services" instead of "Fintech").'
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
64
|
+
return callVerb('/signals/find', input)
|
|
150
65
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const getPlaceReviewsName = 'get_place_reviews'
|
|
6
6
|
|
|
@@ -37,14 +37,5 @@ export const getPlaceReviewsSchema = {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function getPlaceReviewsHandler(input: Record<string, unknown>) {
|
|
40
|
-
|
|
41
|
-
const resolved = resolvePreferredProviders('get_place_reviews', restInput, rawUseProviders)
|
|
42
|
-
if (!resolved.ok) {
|
|
43
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
44
|
-
}
|
|
45
|
-
const result = await executeWithFallback('get_place_reviews', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
46
|
-
if (isExecutionError(result)) {
|
|
47
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
48
|
-
}
|
|
49
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
40
|
+
return callVerb('/places/reviews', input)
|
|
50
41
|
}
|
package/src/tools/search-ads.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const searchAdsName = 'search_ads'
|
|
6
6
|
|
|
@@ -38,14 +38,5 @@ export const searchAdsSchema = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function searchAdsHandler(input: Record<string, unknown>) {
|
|
41
|
-
|
|
42
|
-
const resolved = resolvePreferredProviders('search_ads', restInput, rawUseProviders)
|
|
43
|
-
if (!resolved.ok) {
|
|
44
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
45
|
-
}
|
|
46
|
-
const result = await executeWithFallback('search_ads', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
47
|
-
if (isExecutionError(result)) {
|
|
48
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
49
|
-
}
|
|
50
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
41
|
+
return callVerb('/ads/search', input)
|
|
51
42
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { compactCompaniesPayload } from '../utils/compact-companies.js'
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
5
4
|
|
|
6
5
|
export const searchCompaniesName = 'search_companies'
|
|
7
6
|
|
|
@@ -38,24 +37,5 @@ export const searchCompaniesSchema = {
|
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
export async function searchCompaniesHandler(input: Record<string, unknown>) {
|
|
41
|
-
|
|
42
|
-
const fields: 'compact' | 'verbose' = rawFields === 'verbose' ? 'verbose' : 'compact'
|
|
43
|
-
const resolved = resolvePreferredProviders('search_companies', restInput, rawUseProviders)
|
|
44
|
-
if (!resolved.ok) {
|
|
45
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
46
|
-
}
|
|
47
|
-
const result = await executeWithFallback('search_companies', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
48
|
-
if (isExecutionError(result)) {
|
|
49
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
50
|
-
}
|
|
51
|
-
// Surface the upstream match count (e.g. Apollo's pagination.total_entries) as the
|
|
52
|
-
// total-addressable-market signal, so clients see "matched N, returned M".
|
|
53
|
-
const totalEntries = (result.data as { pagination?: { total_entries?: unknown } })?.pagination?.total_entries
|
|
54
|
-
if (typeof totalEntries === 'number') {
|
|
55
|
-
;(result._meta as Record<string, unknown>).total_entries = totalEntries
|
|
56
|
-
}
|
|
57
|
-
const payload = fields === 'compact'
|
|
58
|
-
? { ...result, data: compactCompaniesPayload(result.data) }
|
|
59
|
-
: result
|
|
60
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(payload) }] }
|
|
40
|
+
return callVerb('/companies/search', input)
|
|
61
41
|
}
|
package/src/tools/search-jobs.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const _CAREER_SITE_ONLY = ['ats_slugs', 'exclude_ats_slugs', 'company_domains', 'exclude_company_domains']
|
|
6
|
-
const _LINKEDIN_ONLY = ['seniority_levels', 'industries', 'organization_slugs', 'exclude_organization_slugs', 'min_employees', 'max_employees', 'easy_apply_only', 'exclude_easy_apply']
|
|
7
|
-
|
|
8
|
-
function _isFilterSet(input: Record<string, unknown>, keys: string[]): boolean {
|
|
9
|
-
return keys.some((k) => {
|
|
10
|
-
const v = input[k]
|
|
11
|
-
return !!v && (!Array.isArray(v) || v.length > 0)
|
|
12
|
-
})
|
|
13
|
-
}
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
14
4
|
|
|
15
5
|
export const searchJobsName = 'search_jobs'
|
|
16
6
|
|
|
@@ -59,22 +49,5 @@ export const searchJobsSchema = {
|
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
export async function searchJobsHandler(input: Record<string, unknown>) {
|
|
62
|
-
|
|
63
|
-
if (_isFilterSet(restInput, _CAREER_SITE_ONLY) && _isFilterSet(restInput, _LINKEDIN_ONLY)) {
|
|
64
|
-
const err = {
|
|
65
|
-
error:
|
|
66
|
-
'Contradictory filters: ATS/domain filters (ats_slugs, company_domains) are Career Site only, ' +
|
|
67
|
-
'while seniority/industry/employee filters are LinkedIn only. Remove one set.',
|
|
68
|
-
}
|
|
69
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(err) }], isError: true }
|
|
70
|
-
}
|
|
71
|
-
const resolved = resolvePreferredProviders('search_jobs', restInput, rawUseProviders)
|
|
72
|
-
if (!resolved.ok) {
|
|
73
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
74
|
-
}
|
|
75
|
-
const result = await executeWithFallback('search_jobs', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
76
|
-
if (isExecutionError(result)) {
|
|
77
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
78
|
-
}
|
|
79
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
52
|
+
return callVerb('/jobs/search', input)
|
|
80
53
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const searchPlacesName = 'search_places'
|
|
6
6
|
|
|
@@ -45,75 +45,8 @@ export const searchPlacesSchema = {
|
|
|
45
45
|
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_places').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function asNumber(v: unknown): number | undefined {
|
|
49
|
-
return typeof v === 'number' && Number.isFinite(v) ? v : undefined
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Google Maps Scraper does not honor min/max rating or review-count filters
|
|
53
|
-
// upstream, so the params would otherwise be silently dropped. Filter client-side
|
|
54
|
-
// against the `totalScore` (rating) and `reviewsCount` fields on each place.
|
|
55
|
-
// Applied to all routes for consistency; on Openmart this is a no-op since the
|
|
56
|
-
// filters were already enforced upstream and Openmart returns `{ data: [...] }`
|
|
57
|
-
// rather than `{ places: [...] }`.
|
|
58
|
-
// Pure: returns a new object instead of mutating its argument.
|
|
59
|
-
export function applyPlaceFilters(
|
|
60
|
-
data: unknown,
|
|
61
|
-
filters: { minRating?: number; maxRating?: number; minReviews?: number; maxReviews?: number },
|
|
62
|
-
): unknown {
|
|
63
|
-
const { minRating, maxRating, minReviews, maxReviews } = filters
|
|
64
|
-
if (minRating === undefined && maxRating === undefined && minReviews === undefined && maxReviews === undefined) return data
|
|
65
|
-
if (!data || typeof data !== 'object') return data
|
|
66
|
-
const d = data as Record<string, unknown>
|
|
67
|
-
const places = d.places
|
|
68
|
-
if (!Array.isArray(places)) return data
|
|
69
|
-
const filtered = places.filter((place) => {
|
|
70
|
-
if (!place || typeof place !== 'object') return false
|
|
71
|
-
const p = place as Record<string, unknown>
|
|
72
|
-
const rating = asNumber(p.totalScore)
|
|
73
|
-
const reviews = asNumber(p.reviewsCount)
|
|
74
|
-
if (minRating !== undefined && (rating === undefined || rating < minRating)) return false
|
|
75
|
-
if (maxRating !== undefined && (rating === undefined || rating > maxRating)) return false
|
|
76
|
-
if (minReviews !== undefined && (reviews === undefined || reviews < minReviews)) return false
|
|
77
|
-
if (maxReviews !== undefined && (reviews === undefined || reviews > maxReviews)) return false
|
|
78
|
-
return true
|
|
79
|
-
})
|
|
80
|
-
return { ...d, places: filtered }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
48
|
export async function searchPlacesHandler(input: Record<string, unknown>) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
88
|
-
}
|
|
89
|
-
const result = await executeWithFallback('search_places', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
90
|
-
if (isExecutionError(result)) {
|
|
91
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
92
|
-
}
|
|
93
|
-
const filters = {
|
|
94
|
-
minRating: asNumber(restInput.min_overall_rating),
|
|
95
|
-
maxRating: asNumber(restInput.max_overall_rating),
|
|
96
|
-
minReviews: asNumber(restInput.min_total_reviews),
|
|
97
|
-
maxReviews: asNumber(restInput.max_total_reviews),
|
|
98
|
-
}
|
|
99
|
-
const scraped = placesCount(result.data)
|
|
100
|
-
result.data = applyPlaceFilters(result.data, filters)
|
|
101
|
-
const matched = placesCount(result.data)
|
|
102
|
-
// Google Maps bills per place scraped upstream (what ColdIQ pays the provider),
|
|
103
|
-
// but rating/review filters are applied here client-side. When the filter trims
|
|
104
|
-
// the set, make the gap explicit so the credit charge isn't surprising.
|
|
105
|
-
if (scraped !== undefined && matched !== undefined && matched < scraped) {
|
|
106
|
-
;(result._meta as Record<string, unknown>).filtered = {
|
|
107
|
-
scraped,
|
|
108
|
-
matched,
|
|
109
|
-
note: 'Rating/review filters are applied client-side. You are billed per place scraped upstream (scraped), not per matched place.',
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function placesCount(data: unknown): number | undefined {
|
|
116
|
-
if (!data || typeof data !== 'object') return undefined
|
|
117
|
-
const places = (data as Record<string, unknown>).places
|
|
118
|
-
return Array.isArray(places) ? places.length : undefined
|
|
49
|
+
// The MCP `provider` (openmart|google_maps) is a place-source pin; the verb has
|
|
50
|
+
// no such input field, so route it through the envelope's provider list.
|
|
51
|
+
return callVerb('/places/search', input, { providerFromField: 'provider' })
|
|
119
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const searchRedditName = 'search_reddit'
|
|
6
6
|
|
|
@@ -34,14 +34,5 @@ export const searchRedditSchema = {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function searchRedditHandler(input: Record<string, unknown>) {
|
|
37
|
-
|
|
38
|
-
const resolved = resolvePreferredProviders('search_reddit', restInput, rawUseProviders)
|
|
39
|
-
if (!resolved.ok) {
|
|
40
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
41
|
-
}
|
|
42
|
-
const result = await executeWithFallback('search_reddit', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
43
|
-
if (isExecutionError(result)) {
|
|
44
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
45
|
-
}
|
|
46
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
37
|
+
return callVerb('/reddit/search', input)
|
|
47
38
|
}
|
package/src/tools/search-seo.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const searchSeoName = 'search_seo'
|
|
6
6
|
|
|
@@ -53,14 +53,5 @@ export const searchSeoSchema = {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export async function searchSeoHandler(input: Record<string, unknown>) {
|
|
56
|
-
|
|
57
|
-
const resolved = resolvePreferredProviders('search_seo', restInput, rawUseProviders)
|
|
58
|
-
if (!resolved.ok) {
|
|
59
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
60
|
-
}
|
|
61
|
-
const result = await executeWithFallback('search_seo', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
62
|
-
if (isExecutionError(result)) {
|
|
63
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
64
|
-
}
|
|
65
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
56
|
+
return callVerb('/seo/search', input)
|
|
66
57
|
}
|
package/src/tools/search-web.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const searchWebName = 'search_web'
|
|
6
6
|
|
|
@@ -19,14 +19,5 @@ export const searchWebSchema = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export async function searchWebHandler(input: Record<string, unknown>) {
|
|
22
|
-
|
|
23
|
-
const resolved = resolvePreferredProviders('search_web', restInput, rawUseProviders)
|
|
24
|
-
if (!resolved.ok) {
|
|
25
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
26
|
-
}
|
|
27
|
-
const result = await executeWithFallback('search_web', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
28
|
-
if (isExecutionError(result)) {
|
|
29
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
30
|
-
}
|
|
31
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
22
|
+
return callVerb('/web/search', input)
|
|
32
23
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const verifyEmailName = 'verify_email'
|
|
6
6
|
|
|
@@ -12,79 +12,6 @@ export const verifyEmailSchema = {
|
|
|
12
12
|
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('verify_email').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
type NormalizedStatus = 'valid' | 'invalid' | 'catch_all' | 'risky' | 'disposable' | 'unknown'
|
|
16
|
-
|
|
17
|
-
const STATUS_ALIASES: Record<string, NormalizedStatus> = {
|
|
18
|
-
valid: 'valid',
|
|
19
|
-
deliverable: 'valid',
|
|
20
|
-
ok: 'valid',
|
|
21
|
-
invalid: 'invalid',
|
|
22
|
-
undeliverable: 'invalid',
|
|
23
|
-
bounce: 'invalid',
|
|
24
|
-
bounced: 'invalid',
|
|
25
|
-
catch_all: 'catch_all',
|
|
26
|
-
catchall: 'catch_all',
|
|
27
|
-
'catch-all': 'catch_all',
|
|
28
|
-
accept_all: 'catch_all',
|
|
29
|
-
acceptall: 'catch_all',
|
|
30
|
-
risky: 'risky',
|
|
31
|
-
disposable: 'disposable',
|
|
32
|
-
unknown: 'unknown',
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeStatus(raw: unknown): NormalizedStatus {
|
|
36
|
-
if (typeof raw !== 'string' || raw.length === 0) return 'unknown'
|
|
37
|
-
return STATUS_ALIASES[raw.toLowerCase()] ?? 'unknown'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Provider responses are heterogeneous. We surface a single `status` field on the
|
|
41
|
-
// payload while leaving the raw upstream fields untouched so callers that already
|
|
42
|
-
// depend on them keep working.
|
|
43
|
-
export function addNormalizedStatus(data: unknown, providerId: string): unknown {
|
|
44
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) return data
|
|
45
|
-
const d = { ...(data as Record<string, unknown>) }
|
|
46
|
-
|
|
47
|
-
// If the response already carries a status string, normalize it and stop.
|
|
48
|
-
if (typeof d.status === 'string') {
|
|
49
|
-
d.status = normalizeStatus(d.status)
|
|
50
|
-
return d
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (providerId === 'findymail') {
|
|
54
|
-
// Findymail's /api/verify returns { email, verified: boolean, provider } — no
|
|
55
|
-
// catch_all/unknown distinction. Map the boolean to valid/invalid only.
|
|
56
|
-
d.status = d.verified === true ? 'valid' : 'invalid'
|
|
57
|
-
return d
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (providerId === 'icypeas') {
|
|
61
|
-
const item = d.item as Record<string, unknown> | undefined
|
|
62
|
-
d.status = normalizeStatus(item?.status)
|
|
63
|
-
return d
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (providerId === 'linkupapi-validate') {
|
|
67
|
-
// LinkupAPI shapes vary; accept any of the common signal fields.
|
|
68
|
-
const truthy = d.verified === true || d.valid === true || d.result === 'valid'
|
|
69
|
-
d.status = truthy ? 'valid' : normalizeStatus(d.result ?? d.valid ?? d.verified)
|
|
70
|
-
return d
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Unknown provider — leave the original payload alone and flag the gap.
|
|
74
|
-
d.status = 'unknown'
|
|
75
|
-
return d
|
|
76
|
-
}
|
|
77
|
-
|
|
78
15
|
export async function verifyEmailHandler(input: Record<string, unknown>) {
|
|
79
|
-
|
|
80
|
-
const resolved = resolvePreferredProviders('verify_email', restInput, rawUseProviders)
|
|
81
|
-
if (!resolved.ok) {
|
|
82
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
83
|
-
}
|
|
84
|
-
const result = await executeWithFallback('verify_email', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
85
|
-
if (isExecutionError(result)) {
|
|
86
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
87
|
-
}
|
|
88
|
-
result.data = addNormalizedStatus(result.data, result._meta.provider)
|
|
89
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
16
|
+
return callVerb('/email/verify', input)
|
|
90
17
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { callApi } from './client.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin client for the deployed /v1 GTM verb endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Each verb takes an envelope `{ input: {...filters}, provider?, fields?, reveal? }`
|
|
7
|
+
* (or `{ inputs: [...] }` for bulk) and returns `{ data, _meta }` (or `{ results }`
|
|
8
|
+
* for bulk, or `{ error, providers_tried }` on failure with a 4xx/5xx status).
|
|
9
|
+
*
|
|
10
|
+
* The MCP tool schemas expose `use_providers` (the ordered provider list) and the
|
|
11
|
+
* optional `fields` / `reveal` knobs alongside the verb's filter fields. This helper
|
|
12
|
+
* splits those out and maps everything else into the verb envelope, so each tool
|
|
13
|
+
* handler collapses to a single delegating call.
|
|
14
|
+
*
|
|
15
|
+
* A few MCP tool schemas predate the verb input schemas and need light mapping:
|
|
16
|
+
* - `remap`: rename input keys before building the envelope (e.g. find_phone's
|
|
17
|
+
* `company_domain` → the verb's `domain`).
|
|
18
|
+
* - `providerFromField`: a tool-specific provider-pin field (e.g. search_places'
|
|
19
|
+
* `provider: openmart|google_maps`) → the envelope's provider list.
|
|
20
|
+
* - `reattachIdField`: bulk verbs return `results` positionally with no echo of the
|
|
21
|
+
* per-item correlation id, so re-attach `input[bulkField][i][id]` onto `results[i]`
|
|
22
|
+
* so callers can still match results back.
|
|
23
|
+
*/
|
|
24
|
+
export interface CallVerbOptions {
|
|
25
|
+
bulkField?: string
|
|
26
|
+
remap?: Record<string, string>
|
|
27
|
+
providerFromField?: string
|
|
28
|
+
reattachIdField?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function callVerb(
|
|
32
|
+
verbPath: string,
|
|
33
|
+
input: Record<string, unknown>,
|
|
34
|
+
opts?: CallVerbOptions,
|
|
35
|
+
): Promise<{ content: [{ type: 'text'; text: string }]; isError?: boolean }> {
|
|
36
|
+
// 1. Rename any schema-drift field names before splitting the envelope.
|
|
37
|
+
let work: Record<string, unknown> = input
|
|
38
|
+
if (opts?.remap) {
|
|
39
|
+
work = {}
|
|
40
|
+
for (const [k, v] of Object.entries(input)) {
|
|
41
|
+
work[opts.remap[k] ?? k] = v
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { use_providers, fields, reveal, ...rest } = work
|
|
46
|
+
|
|
47
|
+
// 2. Resolve the provider pin: explicit use_providers wins; otherwise a
|
|
48
|
+
// tool-specific provider field (e.g. search_places `provider`).
|
|
49
|
+
let providerPin: string[] | undefined =
|
|
50
|
+
Array.isArray(use_providers) && use_providers.length ? (use_providers as string[]) : undefined
|
|
51
|
+
if (opts?.providerFromField && rest[opts.providerFromField] !== undefined) {
|
|
52
|
+
const v = rest[opts.providerFromField]
|
|
53
|
+
delete rest[opts.providerFromField]
|
|
54
|
+
if (!providerPin && typeof v === 'string' && v) providerPin = [v]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. Build the envelope (single vs bulk).
|
|
58
|
+
let envelope: Record<string, unknown>
|
|
59
|
+
let list: Array<Record<string, unknown>> | undefined
|
|
60
|
+
if (opts?.bulkField) {
|
|
61
|
+
const { [opts.bulkField]: l, ...siblings } = rest
|
|
62
|
+
list = Array.isArray(l) ? (l as Array<Record<string, unknown>>) : []
|
|
63
|
+
envelope = { ...siblings, inputs: list }
|
|
64
|
+
} else {
|
|
65
|
+
envelope = { input: rest }
|
|
66
|
+
}
|
|
67
|
+
if (providerPin) envelope.provider = providerPin
|
|
68
|
+
if (fields !== undefined) envelope.fields = fields
|
|
69
|
+
if (reveal !== undefined) envelope.reveal = reveal
|
|
70
|
+
|
|
71
|
+
const res = await callApi('POST', verbPath, envelope)
|
|
72
|
+
|
|
73
|
+
// 4. Re-attach the per-item correlation id to bulk results (positional).
|
|
74
|
+
let data = res.data
|
|
75
|
+
if (
|
|
76
|
+
opts?.reattachIdField && opts?.bulkField && list &&
|
|
77
|
+
data && typeof data === 'object' && Array.isArray((data as { results?: unknown }).results)
|
|
78
|
+
) {
|
|
79
|
+
const idField = opts.reattachIdField
|
|
80
|
+
const results = (data as { results: unknown[] }).results.map((r, i) => {
|
|
81
|
+
const id = list?.[i]?.[idField]
|
|
82
|
+
return id !== undefined && r && typeof r === 'object' && !Array.isArray(r)
|
|
83
|
+
? { [idField]: id, ...(r as Record<string, unknown>) }
|
|
84
|
+
: r
|
|
85
|
+
})
|
|
86
|
+
data = { ...(data as Record<string, unknown>), results }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
91
|
+
isError: !res.ok,
|
|
92
|
+
}
|
|
93
|
+
}
|