@coldiq/mcp 0.3.11 → 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.
Files changed (135) hide show
  1. package/dist/client.d.ts.map +1 -1
  2. package/dist/client.js +4 -0
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/session-usage.d.ts +59 -0
  7. package/dist/session-usage.d.ts.map +1 -0
  8. package/dist/session-usage.js +96 -0
  9. package/dist/session-usage.js.map +1 -0
  10. package/dist/tools/enrich-company.d.ts +3 -9
  11. package/dist/tools/enrich-company.d.ts.map +1 -1
  12. package/dist/tools/enrich-company.js +3 -16
  13. package/dist/tools/enrich-company.js.map +1 -1
  14. package/dist/tools/enrich-person.d.ts +3 -9
  15. package/dist/tools/enrich-person.d.ts.map +1 -1
  16. package/dist/tools/enrich-person.js +3 -12
  17. package/dist/tools/enrich-person.js.map +1 -1
  18. package/dist/tools/fetch-page-content.d.ts +3 -9
  19. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  20. package/dist/tools/fetch-page-content.js +3 -12
  21. package/dist/tools/fetch-page-content.js.map +1 -1
  22. package/dist/tools/find-email.d.ts +3 -9
  23. package/dist/tools/find-email.d.ts.map +1 -1
  24. package/dist/tools/find-email.js +3 -40
  25. package/dist/tools/find-email.js.map +1 -1
  26. package/dist/tools/find-emails.d.ts +3 -4
  27. package/dist/tools/find-emails.d.ts.map +1 -1
  28. package/dist/tools/find-emails.js +8 -423
  29. package/dist/tools/find-emails.js.map +1 -1
  30. package/dist/tools/find-influencers.d.ts +3 -9
  31. package/dist/tools/find-influencers.d.ts.map +1 -1
  32. package/dist/tools/find-influencers.js +3 -12
  33. package/dist/tools/find-influencers.js.map +1 -1
  34. package/dist/tools/find-people.d.ts +3 -8
  35. package/dist/tools/find-people.d.ts.map +1 -1
  36. package/dist/tools/find-people.js +3 -98
  37. package/dist/tools/find-people.js.map +1 -1
  38. package/dist/tools/find-phone.d.ts +3 -9
  39. package/dist/tools/find-phone.d.ts.map +1 -1
  40. package/dist/tools/find-phone.js +4 -41
  41. package/dist/tools/find-phone.js.map +1 -1
  42. package/dist/tools/find-signals.d.ts +3 -9
  43. package/dist/tools/find-signals.d.ts.map +1 -1
  44. package/dist/tools/find-signals.js +3 -83
  45. package/dist/tools/find-signals.js.map +1 -1
  46. package/dist/tools/get-place-reviews.d.ts +3 -9
  47. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  48. package/dist/tools/get-place-reviews.js +3 -12
  49. package/dist/tools/get-place-reviews.js.map +1 -1
  50. package/dist/tools/get-session-usage.d.ts +10 -0
  51. package/dist/tools/get-session-usage.d.ts.map +1 -0
  52. package/dist/tools/get-session-usage.js +14 -0
  53. package/dist/tools/get-session-usage.js.map +1 -0
  54. package/dist/tools/search-ads.d.ts +3 -9
  55. package/dist/tools/search-ads.d.ts.map +1 -1
  56. package/dist/tools/search-ads.js +3 -12
  57. package/dist/tools/search-ads.js.map +1 -1
  58. package/dist/tools/search-companies.d.ts +3 -9
  59. package/dist/tools/search-companies.d.ts.map +1 -1
  60. package/dist/tools/search-companies.js +3 -24
  61. package/dist/tools/search-companies.js.map +1 -1
  62. package/dist/tools/search-jobs.d.ts +3 -9
  63. package/dist/tools/search-jobs.d.ts.map +1 -1
  64. package/dist/tools/search-jobs.js +3 -27
  65. package/dist/tools/search-jobs.js.map +1 -1
  66. package/dist/tools/search-places.d.ts +3 -15
  67. package/dist/tools/search-places.d.ts.map +1 -1
  68. package/dist/tools/search-places.js +5 -76
  69. package/dist/tools/search-places.js.map +1 -1
  70. package/dist/tools/search-reddit.d.ts +3 -9
  71. package/dist/tools/search-reddit.d.ts.map +1 -1
  72. package/dist/tools/search-reddit.js +3 -12
  73. package/dist/tools/search-reddit.js.map +1 -1
  74. package/dist/tools/search-seo.d.ts +3 -9
  75. package/dist/tools/search-seo.d.ts.map +1 -1
  76. package/dist/tools/search-seo.js +3 -12
  77. package/dist/tools/search-seo.js.map +1 -1
  78. package/dist/tools/search-web.d.ts +3 -9
  79. package/dist/tools/search-web.d.ts.map +1 -1
  80. package/dist/tools/search-web.js +3 -12
  81. package/dist/tools/search-web.js.map +1 -1
  82. package/dist/tools/verify-email.d.ts +3 -10
  83. package/dist/tools/verify-email.d.ts.map +1 -1
  84. package/dist/tools/verify-email.js +3 -68
  85. package/dist/tools/verify-email.js.map +1 -1
  86. package/dist/verb-client.d.ts +35 -0
  87. package/dist/verb-client.d.ts.map +1 -0
  88. package/dist/verb-client.js +57 -0
  89. package/dist/verb-client.js.map +1 -0
  90. package/package.json +1 -1
  91. package/src/client.ts +4 -0
  92. package/src/index.ts +8 -0
  93. package/src/session-usage.ts +130 -0
  94. package/src/tools/enrich-company.ts +3 -16
  95. package/src/tools/enrich-person.ts +3 -12
  96. package/src/tools/fetch-page-content.ts +3 -12
  97. package/src/tools/find-email.ts +3 -42
  98. package/src/tools/find-emails.ts +8 -506
  99. package/src/tools/find-influencers.ts +3 -12
  100. package/src/tools/find-people.ts +3 -107
  101. package/src/tools/find-phone.ts +4 -42
  102. package/src/tools/find-signals.ts +3 -88
  103. package/src/tools/get-place-reviews.ts +3 -12
  104. package/src/tools/get-session-usage.ts +18 -0
  105. package/src/tools/search-ads.ts +3 -12
  106. package/src/tools/search-companies.ts +3 -23
  107. package/src/tools/search-jobs.ts +3 -30
  108. package/src/tools/search-places.ts +5 -72
  109. package/src/tools/search-reddit.ts +3 -12
  110. package/src/tools/search-seo.ts +3 -12
  111. package/src/tools/search-web.ts +3 -12
  112. package/src/tools/verify-email.ts +3 -76
  113. package/src/verb-client.ts +93 -0
  114. package/tests/executor.test.ts +69 -0
  115. package/tests/session-usage.test.ts +130 -0
  116. package/tests/tools/get-session-usage.test.ts +44 -0
  117. package/tests/verb-client.test.ts +152 -0
  118. package/tests/tools/enrich-company.test.ts +0 -92
  119. package/tests/tools/enrich-person.test.ts +0 -140
  120. package/tests/tools/fetch-page-content.test.ts +0 -108
  121. package/tests/tools/find-email.test.ts +0 -137
  122. package/tests/tools/find-emails.test.ts +0 -818
  123. package/tests/tools/find-influencers.test.ts +0 -91
  124. package/tests/tools/find-people.test.ts +0 -621
  125. package/tests/tools/find-phone.test.ts +0 -100
  126. package/tests/tools/find-signals.test.ts +0 -175
  127. package/tests/tools/get-place-reviews.test.ts +0 -73
  128. package/tests/tools/search-ads.test.ts +0 -182
  129. package/tests/tools/search-companies.test.ts +0 -192
  130. package/tests/tools/search-jobs.test.ts +0 -210
  131. package/tests/tools/search-places.test.ts +0 -210
  132. package/tests/tools/search-reddit.test.ts +0 -194
  133. package/tests/tools/search-seo.test.ts +0 -183
  134. package/tests/tools/search-web.test.ts +0 -79
  135. package/tests/tools/verify-email.test.ts +0 -142
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Thin client for the deployed /v1 GTM verb endpoints.
3
+ *
4
+ * Each verb takes an envelope `{ input: {...filters}, provider?, fields?, reveal? }`
5
+ * (or `{ inputs: [...] }` for bulk) and returns `{ data, _meta }` (or `{ results }`
6
+ * for bulk, or `{ error, providers_tried }` on failure with a 4xx/5xx status).
7
+ *
8
+ * The MCP tool schemas expose `use_providers` (the ordered provider list) and the
9
+ * optional `fields` / `reveal` knobs alongside the verb's filter fields. This helper
10
+ * splits those out and maps everything else into the verb envelope, so each tool
11
+ * handler collapses to a single delegating call.
12
+ *
13
+ * A few MCP tool schemas predate the verb input schemas and need light mapping:
14
+ * - `remap`: rename input keys before building the envelope (e.g. find_phone's
15
+ * `company_domain` → the verb's `domain`).
16
+ * - `providerFromField`: a tool-specific provider-pin field (e.g. search_places'
17
+ * `provider: openmart|google_maps`) → the envelope's provider list.
18
+ * - `reattachIdField`: bulk verbs return `results` positionally with no echo of the
19
+ * per-item correlation id, so re-attach `input[bulkField][i][id]` onto `results[i]`
20
+ * so callers can still match results back.
21
+ */
22
+ export interface CallVerbOptions {
23
+ bulkField?: string;
24
+ remap?: Record<string, string>;
25
+ providerFromField?: string;
26
+ reattachIdField?: string;
27
+ }
28
+ export declare function callVerb(verbPath: string, input: Record<string, unknown>, opts?: CallVerbOptions): Promise<{
29
+ content: [{
30
+ type: 'text';
31
+ text: string;
32
+ }];
33
+ isError?: boolean;
34
+ }>;
35
+ //# sourceMappingURL=verb-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verb-client.d.ts","sourceRoot":"","sources":["../src/verb-client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA0D3E"}
@@ -0,0 +1,57 @@
1
+ import { callApi } from './client.js';
2
+ export async function callVerb(verbPath, input, opts) {
3
+ // 1. Rename any schema-drift field names before splitting the envelope.
4
+ let work = input;
5
+ if (opts?.remap) {
6
+ work = {};
7
+ for (const [k, v] of Object.entries(input)) {
8
+ work[opts.remap[k] ?? k] = v;
9
+ }
10
+ }
11
+ const { use_providers, fields, reveal, ...rest } = work;
12
+ // 2. Resolve the provider pin: explicit use_providers wins; otherwise a
13
+ // tool-specific provider field (e.g. search_places `provider`).
14
+ let providerPin = Array.isArray(use_providers) && use_providers.length ? use_providers : undefined;
15
+ if (opts?.providerFromField && rest[opts.providerFromField] !== undefined) {
16
+ const v = rest[opts.providerFromField];
17
+ delete rest[opts.providerFromField];
18
+ if (!providerPin && typeof v === 'string' && v)
19
+ providerPin = [v];
20
+ }
21
+ // 3. Build the envelope (single vs bulk).
22
+ let envelope;
23
+ let list;
24
+ if (opts?.bulkField) {
25
+ const { [opts.bulkField]: l, ...siblings } = rest;
26
+ list = Array.isArray(l) ? l : [];
27
+ envelope = { ...siblings, inputs: list };
28
+ }
29
+ else {
30
+ envelope = { input: rest };
31
+ }
32
+ if (providerPin)
33
+ envelope.provider = providerPin;
34
+ if (fields !== undefined)
35
+ envelope.fields = fields;
36
+ if (reveal !== undefined)
37
+ envelope.reveal = reveal;
38
+ const res = await callApi('POST', verbPath, envelope);
39
+ // 4. Re-attach the per-item correlation id to bulk results (positional).
40
+ let data = res.data;
41
+ if (opts?.reattachIdField && opts?.bulkField && list &&
42
+ data && typeof data === 'object' && Array.isArray(data.results)) {
43
+ const idField = opts.reattachIdField;
44
+ const results = data.results.map((r, i) => {
45
+ const id = list?.[i]?.[idField];
46
+ return id !== undefined && r && typeof r === 'object' && !Array.isArray(r)
47
+ ? { [idField]: id, ...r }
48
+ : r;
49
+ });
50
+ data = { ...data, results };
51
+ }
52
+ return {
53
+ content: [{ type: 'text', text: JSON.stringify(data) }],
54
+ isError: !res.ok,
55
+ };
56
+ }
57
+ //# sourceMappingURL=verb-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verb-client.js","sourceRoot":"","sources":["../src/verb-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldiq/mcp",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Agent, setGlobalDispatcher } from 'undici'
2
+ import { recordChargeFromHeaders } from './session-usage.js'
2
3
 
3
4
  export type ApiResponse = {
4
5
  ok: boolean
@@ -75,6 +76,9 @@ export async function callApi(
75
76
  res.headers.forEach((value, key) => {
76
77
  headers[key.toLowerCase()] = value
77
78
  })
79
+ // Tally this call's credit charge for the per-session usage report. No-op
80
+ // unless the response carries a charge header, so polls/errors are skipped.
81
+ recordChargeFromHeaders(path, headers)
78
82
  return { ok: res.ok, status: res.status, data, headers }
79
83
  } catch (err) {
80
84
  const isTimeout =
package/src/index.ts CHANGED
@@ -130,6 +130,13 @@ import {
130
130
  getCreditBalanceHandler,
131
131
  } from './tools/get-credit-balance.js'
132
132
 
133
+ import {
134
+ getSessionUsageName,
135
+ getSessionUsageDescription,
136
+ getSessionUsageSchema,
137
+ getSessionUsageHandler,
138
+ } from './tools/get-session-usage.js'
139
+
133
140
  import {
134
141
  extractPostEngagementName,
135
142
  extractPostEngagementDescription,
@@ -192,6 +199,7 @@ server.tool(searchSeoName, searchSeoDescription, searchSeoSchema, searchSeoHandl
192
199
  server.tool(findSignalsName, findSignalsDescription, findSignalsSchema, findSignalsHandler)
193
200
  server.tool(fetchPageContentName, fetchPageContentDescription, fetchPageContentSchema, fetchPageContentHandler)
194
201
  server.tool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler)
202
+ server.tool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler)
195
203
  server.tool(listDataSourcesName, listDataSourcesDescription, listDataSourcesSchema, listDataSourcesHandler)
196
204
  server.tool(extractPostEngagementName, extractPostEngagementDescription, extractPostEngagementSchema, extractPostEngagementHandler)
197
205
  server.tool(getPlaceReviewsName, getPlaceReviewsDescription, getPlaceReviewsSchema, getPlaceReviewsHandler)
@@ -0,0 +1,130 @@
1
+ /**
2
+ * In-process ColdIQ credit accumulator for one MCP session ("campaign").
3
+ *
4
+ * The ColdIQ MCP server runs as a stdio subprocess spawned per client
5
+ * conversation, so a module-level singleton here is naturally scoped to a
6
+ * single campaign: it starts empty when the process boots and is discarded
7
+ * when the conversation's server exits. We tally the per-call charges the API
8
+ * already reports via the `X-ColdIQ-Credits-Charged` / `-Remaining` response
9
+ * headers (the same signal the executor surfaces in `_meta`), so reporting
10
+ * session spend never issues an extra billable call.
11
+ *
12
+ * Caveats this module deliberately accepts (and the tool surfaces to the user):
13
+ * - It only counts calls made during the current process lifetime; resuming or
14
+ * restarting the conversation starts a fresh tally.
15
+ * - `charged` reflects what the API billed at call time. For async jobs that is
16
+ * the submit-time reservation; later webhook settlement is not visible here.
17
+ */
18
+
19
+ const CHARGED_HEADER = 'x-coldiq-credits-charged'
20
+ const REMAINING_HEADER = 'x-coldiq-credits-remaining'
21
+
22
+ export interface EndpointUsage {
23
+ /** Number of billed calls to this endpoint this session. */
24
+ calls: number
25
+ /** Total net ColdIQ credits charged across those calls. */
26
+ credits: number
27
+ }
28
+
29
+ export interface SessionUsageSnapshot {
30
+ /** ISO 8601 timestamp of when this MCP session (process) started. */
31
+ startedAt: string
32
+ /** Milliseconds elapsed since the session started. */
33
+ durationMs: number
34
+ /** Count of calls that charged credits this session. */
35
+ billedCalls: number
36
+ /** Sum of net credits charged across all billed calls this session. */
37
+ totalCreditsCharged: number
38
+ /** Balance the API reported after the most recent billed call, if any. */
39
+ balanceAfterLastCharge?: number
40
+ /** Per-endpoint breakdown, highest spend first. */
41
+ byEndpoint: Array<{ endpoint: string; calls: number; credits: number }>
42
+ }
43
+
44
+ interface MutableState {
45
+ startedAt: number
46
+ billedCalls: number
47
+ totalCreditsCharged: number
48
+ balanceAfterLastCharge?: number
49
+ byEndpoint: Map<string, EndpointUsage>
50
+ }
51
+
52
+ const state: MutableState = {
53
+ startedAt: Date.now(),
54
+ billedCalls: 0,
55
+ totalCreditsCharged: 0,
56
+ byEndpoint: new Map(),
57
+ }
58
+
59
+ function parseHeaderNumber(raw: string | undefined): number | undefined {
60
+ if (raw === undefined) return undefined
61
+ const n = Number(raw)
62
+ return Number.isFinite(n) ? n : undefined
63
+ }
64
+
65
+ // Credits can be fractional (settlement refunds). Round to 4 dp so summed
66
+ // floating-point noise never leaks into the reported totals.
67
+ function round(n: number): number {
68
+ return Math.round(n * 10000) / 10000
69
+ }
70
+
71
+ /**
72
+ * Record a single billed call. `charged` is the net credits for this call;
73
+ * `remaining`, when present, is the authoritative balance afterwards.
74
+ * Non-finite or negative charges are ignored so a malformed header can't
75
+ * corrupt the running total.
76
+ */
77
+ export function recordCharge(endpoint: string, charged: number, remaining?: number): void {
78
+ if (!Number.isFinite(charged) || charged < 0) return
79
+ state.billedCalls += 1
80
+ state.totalCreditsCharged += charged
81
+ if (remaining !== undefined && Number.isFinite(remaining)) {
82
+ state.balanceAfterLastCharge = remaining
83
+ }
84
+ const existing = state.byEndpoint.get(endpoint)
85
+ if (existing) {
86
+ existing.calls += 1
87
+ existing.credits += charged
88
+ } else {
89
+ state.byEndpoint.set(endpoint, { calls: 1, credits: charged })
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Record a charge from a response's (lowercased) header map. A no-op when the
95
+ * response carries no charge header — polls, errors, and unbilled routes are
96
+ * silently skipped, so this is safe to call after every request.
97
+ */
98
+ export function recordChargeFromHeaders(endpoint: string, headers: Record<string, string>): void {
99
+ const charged = parseHeaderNumber(headers[CHARGED_HEADER])
100
+ if (charged === undefined) return
101
+ recordCharge(endpoint, charged, parseHeaderNumber(headers[REMAINING_HEADER]))
102
+ }
103
+
104
+ /** Build a snapshot of session spend so far. `now` is injectable for tests. */
105
+ export function getSessionUsage(now: number = Date.now()): SessionUsageSnapshot {
106
+ const byEndpoint = Array.from(state.byEndpoint.entries())
107
+ .map(([endpoint, u]) => ({ endpoint, calls: u.calls, credits: round(u.credits) }))
108
+ .sort((a, b) => b.credits - a.credits)
109
+
110
+ const snap: SessionUsageSnapshot = {
111
+ startedAt: new Date(state.startedAt).toISOString(),
112
+ durationMs: now - state.startedAt,
113
+ billedCalls: state.billedCalls,
114
+ totalCreditsCharged: round(state.totalCreditsCharged),
115
+ byEndpoint,
116
+ }
117
+ if (state.balanceAfterLastCharge !== undefined) {
118
+ snap.balanceAfterLastCharge = state.balanceAfterLastCharge
119
+ }
120
+ return snap
121
+ }
122
+
123
+ /** Test-only: reset the accumulator to a known clean state. */
124
+ export function resetSessionUsage(startedAt: number = Date.now()): void {
125
+ state.startedAt = startedAt
126
+ state.billedCalls = 0
127
+ state.totalCreditsCharged = 0
128
+ state.balanceAfterLastCharge = undefined
129
+ state.byEndpoint.clear()
130
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { executeWithFallback, isExecutionError } from '../executor.js'
3
- import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
2
+ import { callVerb } from '../verb-client.js'
3
+ import { getProvidersForCapability } from '../utils/provider-resolver.js'
4
4
 
5
5
  export const enrichCompanyName = 'enrich_company'
6
6
 
@@ -15,18 +15,5 @@ export const enrichCompanySchema = {
15
15
  }
16
16
 
17
17
  export async function enrichCompanyHandler(input: Record<string, unknown>) {
18
- const { use_providers: rawUseProviders, ...restInput } = input
19
- if (!restInput.domain && !restInput.linkedin_url && !restInput.name) {
20
- const err = { error: 'At least one of domain, linkedin_url, or name is required', providers_tried: [] }
21
- return { content: [{ type: 'text' as const, text: JSON.stringify(err) }], isError: true }
22
- }
23
- const resolved = resolvePreferredProviders('enrich_company', 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('enrich_company', 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) }] }
18
+ return callVerb('/company/enrich', input)
32
19
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { executeWithFallback, isExecutionError } from '../executor.js'
3
- import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
2
+ import { callVerb } from '../verb-client.js'
3
+ import { getProvidersForCapability } from '../utils/provider-resolver.js'
4
4
 
5
5
  export const enrichPersonName = 'enrich_person'
6
6
 
@@ -22,14 +22,5 @@ export const enrichPersonSchema = {
22
22
  }
23
23
 
24
24
  export async function enrichPersonHandler(input: Record<string, unknown>) {
25
- const { use_providers: rawUseProviders, ...restInput } = input
26
- const resolved = resolvePreferredProviders('enrich_person', restInput, rawUseProviders)
27
- if (!resolved.ok) {
28
- return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
29
- }
30
- const result = await executeWithFallback('enrich_person', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
31
- if (isExecutionError(result)) {
32
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
33
- }
34
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
25
+ return callVerb('/person/enrich', input)
35
26
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { executeWithFallback, isExecutionError } from '../executor.js'
3
- import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
2
+ import { callVerb } from '../verb-client.js'
3
+ import { getProvidersForCapability } from '../utils/provider-resolver.js'
4
4
 
5
5
  export const fetchPageContentName = 'fetch_page_content'
6
6
 
@@ -30,14 +30,5 @@ export const fetchPageContentSchema = {
30
30
  }
31
31
 
32
32
  export async function fetchPageContentHandler(input: Record<string, unknown>) {
33
- const { use_providers: rawUseProviders, ...restInput } = input
34
- const resolved = resolvePreferredProviders('fetch_page_content', restInput, rawUseProviders)
35
- if (!resolved.ok) {
36
- return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
37
- }
38
- const result = await executeWithFallback('fetch_page_content', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
39
- if (isExecutionError(result)) {
40
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
41
- }
42
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
33
+ return callVerb('/web/fetch', input)
43
34
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
- import { executeWithFallback, isExecutionError } from '../executor.js'
3
- import { resolvePreferredProviders, getProvidersForCapability } from '../utils/provider-resolver.js'
2
+ import { callVerb } from '../verb-client.js'
3
+ import { getProvidersForCapability } from '../utils/provider-resolver.js'
4
4
 
5
5
  export const findEmailName = 'find_email'
6
6
 
@@ -19,45 +19,6 @@ export const findEmailSchema = {
19
19
  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.`),
20
20
  }
21
21
 
22
- const inputSchema = z
23
- .object({
24
- first_name: z.string().optional(),
25
- last_name: z.string().optional(),
26
- domain: z.string().optional(),
27
- company_name: z.string().optional(),
28
- linkedin_url: z.string().optional(),
29
- })
30
- .superRefine((val, ctx) => {
31
- const hasLinkedIn = typeof val.linkedin_url === 'string' && val.linkedin_url.length > 0
32
- const hasName = typeof val.first_name === 'string' && val.first_name.length > 0
33
- const hasDomain =
34
- (typeof val.domain === 'string' && val.domain.length > 0) ||
35
- (typeof val.company_name === 'string' && val.company_name.length > 0)
36
- if (!hasLinkedIn && !(hasName && hasDomain)) {
37
- ctx.addIssue({
38
- code: 'custom',
39
- message: 'Provide either linkedin_url or both first_name and domain/company_name',
40
- })
41
- }
42
- })
43
-
44
22
  export async function findEmailHandler(input: Record<string, unknown>) {
45
- const { use_providers: rawUseProviders, ...restInput } = input
46
- const resolved = resolvePreferredProviders('find_email', restInput, rawUseProviders)
47
- if (!resolved.ok) {
48
- return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
49
- }
50
- const validation = inputSchema.safeParse(restInput)
51
- if (!validation.success) {
52
- const msg = validation.error.issues.map((i) => i.message).join('; ')
53
- return {
54
- content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }) }],
55
- isError: true,
56
- }
57
- }
58
- const result = await executeWithFallback('find_email', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
59
- if (isExecutionError(result)) {
60
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
61
- }
62
- return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
23
+ return callVerb('/email/find', input)
63
24
  }