prospeo-cli 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.
- package/AGENTS.md +429 -0
- package/CLAUDE.md +108 -0
- package/README.md +84 -0
- package/dist/index.js +910 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +626 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +52 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/core/config.ts
|
|
8
|
+
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
var CONFIG_DIR = join(homedir(), ".prospeo");
|
|
12
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
13
|
+
async function loadConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const content = await readFile(CONFIG_FILE, "utf-8");
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/core/errors.ts
|
|
23
|
+
var ProspeoError = class extends Error {
|
|
24
|
+
constructor(message, code, statusCode) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.statusCode = statusCode;
|
|
28
|
+
this.name = "ProspeoError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var AuthError = class extends ProspeoError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message, "AUTH_ERROR", 401);
|
|
34
|
+
this.name = "AuthError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ValidationError = class extends ProspeoError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message, "VALIDATION_ERROR", 400);
|
|
40
|
+
this.name = "ValidationError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var RateLimitError = class extends ProspeoError {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message, "RATE_LIMIT", 429);
|
|
46
|
+
this.name = "RateLimitError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var InsufficientCreditsError = class extends ProspeoError {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message, "INSUFFICIENT_CREDITS", 400);
|
|
52
|
+
this.name = "InsufficientCreditsError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var NoMatchError = class extends ProspeoError {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message, "NO_MATCH", 400);
|
|
58
|
+
this.name = "NoMatchError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var ServerError = class extends ProspeoError {
|
|
62
|
+
constructor(message, statusCode = 500) {
|
|
63
|
+
super(message, "SERVER_ERROR", statusCode);
|
|
64
|
+
this.name = "ServerError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/core/auth.ts
|
|
69
|
+
async function resolveApiKey(flagKey) {
|
|
70
|
+
if (flagKey) return flagKey;
|
|
71
|
+
const envKey = process.env.PROSPEO_API_KEY;
|
|
72
|
+
if (envKey) return envKey;
|
|
73
|
+
const config = await loadConfig();
|
|
74
|
+
if (config?.api_key) return config.api_key;
|
|
75
|
+
throw new AuthError(
|
|
76
|
+
"No API key found. Set PROSPEO_API_KEY, use --api-key, or run: prospeo login"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/core/client.ts
|
|
81
|
+
var BASE_URL = "https://api.prospeo.io";
|
|
82
|
+
var MAX_RETRIES = 3;
|
|
83
|
+
var REQUEST_TIMEOUT = 3e4;
|
|
84
|
+
var VERSION = "0.1.0";
|
|
85
|
+
var ProspeoClient = class {
|
|
86
|
+
apiKey;
|
|
87
|
+
baseUrl;
|
|
88
|
+
maxRetries;
|
|
89
|
+
timeout;
|
|
90
|
+
constructor(options) {
|
|
91
|
+
this.apiKey = options.apiKey;
|
|
92
|
+
this.baseUrl = options.baseUrl ?? BASE_URL;
|
|
93
|
+
this.maxRetries = options.maxRetries ?? MAX_RETRIES;
|
|
94
|
+
this.timeout = options.timeout ?? REQUEST_TIMEOUT;
|
|
95
|
+
}
|
|
96
|
+
async request(options) {
|
|
97
|
+
const url = `${this.baseUrl}${options.path}`;
|
|
98
|
+
const headers = {
|
|
99
|
+
"X-KEY": this.apiKey,
|
|
100
|
+
"User-Agent": `prospeo-cli/${VERSION}`,
|
|
101
|
+
Accept: "application/json"
|
|
102
|
+
};
|
|
103
|
+
if (options.body !== void 0) {
|
|
104
|
+
headers["Content-Type"] = "application/json";
|
|
105
|
+
}
|
|
106
|
+
let lastError;
|
|
107
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
108
|
+
try {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
111
|
+
const response = await fetch(url, {
|
|
112
|
+
method: options.method,
|
|
113
|
+
headers,
|
|
114
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
115
|
+
signal: controller.signal
|
|
116
|
+
});
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
parsed = { error: true, error_code: "PARSE_ERROR", message: text };
|
|
124
|
+
}
|
|
125
|
+
if (response.ok) {
|
|
126
|
+
if (parsed?.error === true) {
|
|
127
|
+
throw this.parseApiError(parsed, response.status);
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
throw this.parseApiError(parsed, response.status);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof ProspeoError) {
|
|
134
|
+
if ((error instanceof RateLimitError || error instanceof ServerError) && attempt < this.maxRetries) {
|
|
135
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
136
|
+
await sleep(delay);
|
|
137
|
+
lastError = error;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const isAbort = error instanceof Error && (error.name === "AbortError" || String(error.message).includes("aborted"));
|
|
143
|
+
if (isAbort) {
|
|
144
|
+
lastError = new ProspeoError(
|
|
145
|
+
`Request timed out after ${this.timeout / 1e3}s: ${options.method} ${options.path}`,
|
|
146
|
+
"TIMEOUT"
|
|
147
|
+
);
|
|
148
|
+
if (attempt < this.maxRetries) {
|
|
149
|
+
await sleep(Math.min(1e3 * Math.pow(2, attempt), 1e4));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw lastError;
|
|
153
|
+
}
|
|
154
|
+
if (error instanceof TypeError && String(error.message).includes("fetch")) {
|
|
155
|
+
throw new ProspeoError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
throw lastError ?? new ProspeoError("Request failed after retries", "MAX_RETRIES");
|
|
161
|
+
}
|
|
162
|
+
parseApiError(body, statusCode) {
|
|
163
|
+
const code = body?.error_code ?? body?.code ?? "API_ERROR";
|
|
164
|
+
const message = body?.message ?? body?.error_message ?? body?.error ?? `API error (HTTP ${statusCode})`;
|
|
165
|
+
switch (code) {
|
|
166
|
+
case "INVALID_API_KEY":
|
|
167
|
+
return new AuthError(message);
|
|
168
|
+
case "INSUFFICIENT_CREDITS":
|
|
169
|
+
return new InsufficientCreditsError(message);
|
|
170
|
+
case "NO_MATCH":
|
|
171
|
+
case "NO_RESULTS":
|
|
172
|
+
return new NoMatchError(message);
|
|
173
|
+
case "RATE_LIMITED":
|
|
174
|
+
return new RateLimitError(message);
|
|
175
|
+
case "INVALID_DATAPOINTS":
|
|
176
|
+
case "INVALID_FILTERS":
|
|
177
|
+
case "INVALID_REQUEST":
|
|
178
|
+
return new ValidationError(message);
|
|
179
|
+
case "INTERNAL_ERROR":
|
|
180
|
+
return new ServerError(message, statusCode);
|
|
181
|
+
default:
|
|
182
|
+
if (statusCode === 401) return new AuthError(message);
|
|
183
|
+
if (statusCode === 429) return new RateLimitError(message);
|
|
184
|
+
if (statusCode >= 500) return new ServerError(message, statusCode);
|
|
185
|
+
return new ProspeoError(message, code, statusCode);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async post(path, body) {
|
|
189
|
+
return this.request({ method: "POST", path, body });
|
|
190
|
+
}
|
|
191
|
+
async get(path) {
|
|
192
|
+
return this.request({ method: "GET", path });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function sleep(ms) {
|
|
196
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/commands/person/enrich.ts
|
|
200
|
+
import { z } from "zod";
|
|
201
|
+
var personEnrichCommand = {
|
|
202
|
+
name: "person_enrich",
|
|
203
|
+
group: "person",
|
|
204
|
+
subcommand: "enrich",
|
|
205
|
+
description: "Enrich a person with verified contact details, email, mobile, job history, and company data. Provide at least one of: linkedin_url, email, person_id, or (first_name/last_name + company identifier).",
|
|
206
|
+
examples: [
|
|
207
|
+
'prospeo person enrich --linkedin-url "https://linkedin.com/in/jdoe"',
|
|
208
|
+
'prospeo person enrich --email "john@acme.com"',
|
|
209
|
+
'prospeo person enrich --first-name "Eva" --last-name "Kiegler" --company-website "intercom.com"',
|
|
210
|
+
'prospeo person enrich --first-name "John" --last-name "Doe" --company-name "Acme" --enrich-mobile',
|
|
211
|
+
'prospeo person enrich --linkedin-url "https://linkedin.com/in/jdoe" --only-verified-email --pretty'
|
|
212
|
+
],
|
|
213
|
+
inputSchema: z.object({
|
|
214
|
+
linkedin_url: z.string().url().optional().describe("LinkedIn profile URL"),
|
|
215
|
+
email: z.string().email().optional().describe("Work email address"),
|
|
216
|
+
first_name: z.string().optional().describe("First name"),
|
|
217
|
+
last_name: z.string().optional().describe("Last name"),
|
|
218
|
+
full_name: z.string().optional().describe("Full name"),
|
|
219
|
+
company_name: z.string().optional().describe("Company name"),
|
|
220
|
+
company_website: z.string().optional().describe("Company website domain (e.g. intercom.com)"),
|
|
221
|
+
company_linkedin_url: z.string().url().optional().describe("Company's LinkedIn URL"),
|
|
222
|
+
person_id: z.string().optional().describe("Person ID from search-person results"),
|
|
223
|
+
only_verified_email: z.preprocess((v) => v === true || v === "true", z.boolean()).optional().describe("Only return records with a verified email"),
|
|
224
|
+
enrich_mobile: z.preprocess((v) => v === true || v === "true", z.boolean()).optional().describe("Also enrich mobile phone number (costs 10 credits instead of 1)"),
|
|
225
|
+
only_verified_mobile: z.preprocess((v) => v === true || v === "true", z.boolean()).optional().describe("Only return records with a verified mobile (requires enrich_mobile)")
|
|
226
|
+
}),
|
|
227
|
+
cliMappings: {
|
|
228
|
+
options: [
|
|
229
|
+
{ field: "linkedin_url", flags: "--linkedin-url <url>", description: "LinkedIn profile URL" },
|
|
230
|
+
{ field: "email", flags: "--email <email>", description: "Work email address" },
|
|
231
|
+
{ field: "first_name", flags: "--first-name <name>", description: "First name" },
|
|
232
|
+
{ field: "last_name", flags: "--last-name <name>", description: "Last name" },
|
|
233
|
+
{ field: "full_name", flags: "--full-name <name>", description: "Full name" },
|
|
234
|
+
{ field: "company_name", flags: "--company-name <name>", description: "Company name" },
|
|
235
|
+
{ field: "company_website", flags: "--company-website <domain>", description: "Company website (e.g. intercom.com)" },
|
|
236
|
+
{ field: "company_linkedin_url", flags: "--company-linkedin-url <url>", description: "Company LinkedIn URL" },
|
|
237
|
+
{ field: "person_id", flags: "--person-id <id>", description: "Person ID from search results" },
|
|
238
|
+
{ field: "only_verified_email", flags: "--only-verified-email", description: "Only return records with verified email" },
|
|
239
|
+
{ field: "enrich_mobile", flags: "--enrich-mobile", description: "Enrich mobile number (10 credits)" },
|
|
240
|
+
{ field: "only_verified_mobile", flags: "--only-verified-mobile", description: "Only return records with verified mobile" }
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
handler: async (input, client) => {
|
|
244
|
+
const { only_verified_email, enrich_mobile, only_verified_mobile, ...dataFields } = input;
|
|
245
|
+
const data = {};
|
|
246
|
+
if (dataFields.linkedin_url) data.linkedin_url = dataFields.linkedin_url;
|
|
247
|
+
if (dataFields.email) data.email = dataFields.email;
|
|
248
|
+
if (dataFields.first_name) data.first_name = dataFields.first_name;
|
|
249
|
+
if (dataFields.last_name) data.last_name = dataFields.last_name;
|
|
250
|
+
if (dataFields.full_name) data.full_name = dataFields.full_name;
|
|
251
|
+
if (dataFields.company_name) data.company_name = dataFields.company_name;
|
|
252
|
+
if (dataFields.company_website) data.company_website = dataFields.company_website;
|
|
253
|
+
if (dataFields.company_linkedin_url) data.company_linkedin_url = dataFields.company_linkedin_url;
|
|
254
|
+
if (dataFields.person_id) data.person_id = dataFields.person_id;
|
|
255
|
+
const body = { data };
|
|
256
|
+
if (only_verified_email !== void 0) body.only_verified_email = only_verified_email;
|
|
257
|
+
if (enrich_mobile !== void 0) body.enrich_mobile = enrich_mobile;
|
|
258
|
+
if (only_verified_mobile !== void 0) body.only_verified_mobile = only_verified_mobile;
|
|
259
|
+
return client.post("/enrich-person", body);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/commands/person/bulk-enrich.ts
|
|
264
|
+
import { z as z2 } from "zod";
|
|
265
|
+
var PersonRecord = z2.object({
|
|
266
|
+
identifier: z2.string().describe("Unique alpha-numeric string to track this record in the response"),
|
|
267
|
+
linkedin_url: z2.string().url().optional(),
|
|
268
|
+
email: z2.string().email().optional(),
|
|
269
|
+
first_name: z2.string().optional(),
|
|
270
|
+
last_name: z2.string().optional(),
|
|
271
|
+
full_name: z2.string().optional(),
|
|
272
|
+
company_name: z2.string().optional(),
|
|
273
|
+
company_website: z2.string().optional(),
|
|
274
|
+
company_linkedin_url: z2.string().url().optional(),
|
|
275
|
+
person_id: z2.string().optional()
|
|
276
|
+
});
|
|
277
|
+
var personBulkEnrichCommand = {
|
|
278
|
+
name: "person_bulk_enrich",
|
|
279
|
+
group: "person",
|
|
280
|
+
subcommand: "bulk-enrich",
|
|
281
|
+
description: 'Enrich up to 50 persons in a single request. Pass a JSON array of person objects via --data. Each object requires an "identifier" field for tracking. Returns matched[], not_matched[], and invalid_datapoints[] arrays.',
|
|
282
|
+
examples: [
|
|
283
|
+
`prospeo person bulk-enrich --data '[{"identifier":"1","linkedin_url":"https://linkedin.com/in/jdoe"},{"identifier":"2","email":"jane@acme.com"}]'`,
|
|
284
|
+
`prospeo person bulk-enrich --data '[{"identifier":"1","first_name":"John","last_name":"Doe","company_website":"acme.com"}]' --only-verified-email`,
|
|
285
|
+
`prospeo person bulk-enrich --data '[{"identifier":"1","linkedin_url":"https://linkedin.com/in/jdoe"}]' --enrich-mobile --pretty`
|
|
286
|
+
],
|
|
287
|
+
inputSchema: z2.object({
|
|
288
|
+
data: z2.preprocess(
|
|
289
|
+
(v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
290
|
+
z2.array(PersonRecord).min(1).max(50)
|
|
291
|
+
).describe('JSON array of up to 50 person objects, each with a unique "identifier" field'),
|
|
292
|
+
only_verified_email: z2.preprocess((v) => v === true || v === "true", z2.boolean()).optional().describe("Only return records with a verified email"),
|
|
293
|
+
enrich_mobile: z2.preprocess((v) => v === true || v === "true", z2.boolean()).optional().describe("Also enrich mobile phone number"),
|
|
294
|
+
only_verified_mobile: z2.preprocess((v) => v === true || v === "true", z2.boolean()).optional().describe("Only return records with a verified mobile")
|
|
295
|
+
}),
|
|
296
|
+
cliMappings: {
|
|
297
|
+
options: [
|
|
298
|
+
{ field: "data", flags: "--data <json>", description: 'JSON array of person objects (each needs "identifier")' },
|
|
299
|
+
{ field: "only_verified_email", flags: "--only-verified-email", description: "Only return records with verified email" },
|
|
300
|
+
{ field: "enrich_mobile", flags: "--enrich-mobile", description: "Enrich mobile numbers" },
|
|
301
|
+
{ field: "only_verified_mobile", flags: "--only-verified-mobile", description: "Only return records with verified mobile" }
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
handler: async (input, client) => {
|
|
305
|
+
const { data, only_verified_email, enrich_mobile, only_verified_mobile } = input;
|
|
306
|
+
const body = { data };
|
|
307
|
+
if (only_verified_email !== void 0) body.only_verified_email = only_verified_email;
|
|
308
|
+
if (enrich_mobile !== void 0) body.enrich_mobile = enrich_mobile;
|
|
309
|
+
if (only_verified_mobile !== void 0) body.only_verified_mobile = only_verified_mobile;
|
|
310
|
+
return client.post("/bulk-enrich-person", body);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/commands/person/search.ts
|
|
315
|
+
import { z as z3 } from "zod";
|
|
316
|
+
var personSearchCommand = {
|
|
317
|
+
name: "person_search",
|
|
318
|
+
group: "person",
|
|
319
|
+
subcommand: "search",
|
|
320
|
+
description: 'Search 200M+ persons with 30+ filters. Pass filters as a JSON object via --filters. Use --page for pagination (25 results/page, max 1000 pages). 1 credit per page returned. Use "prospeo suggestions location" and "prospeo suggestions job-title" to find exact filter values.',
|
|
321
|
+
examples: [
|
|
322
|
+
`prospeo person search --filters '{"person_seniority":{"include":["C_SUITE","VP"]},"company_industry":{"include":["TECHNOLOGY"]}}' --pretty`,
|
|
323
|
+
`prospeo person search --filters '{"company_websites":["stripe.com","brex.com"]}' --page 2`,
|
|
324
|
+
`prospeo person search --filters '{"person_job_title":["Head of Sales"],"company_location":["United States"]}' --pretty`,
|
|
325
|
+
`prospeo person search --filters '{"person_seniority":{"include":["DIRECTOR"]},"company_employee_range":{"include":["51_200","201_500"]}}' --fields "person,company"`
|
|
326
|
+
],
|
|
327
|
+
inputSchema: z3.object({
|
|
328
|
+
filters: z3.preprocess(
|
|
329
|
+
(v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
330
|
+
z3.record(z3.unknown())
|
|
331
|
+
).describe(
|
|
332
|
+
"JSON object of search filters. Common keys: person_seniority (include/exclude arrays), person_job_title (array), company_industry (include/exclude), company_location (array), person_location (array), company_websites (array, max 500), company_names (array, max 500), company_employee_range (include/exclude), person_year_of_experience (min/max), person_department (include/exclude). Use search-suggestions for exact location/job-title values."
|
|
333
|
+
),
|
|
334
|
+
page: z3.coerce.number().min(1).max(1e3).default(1).describe("Page number (1\u20131000, 25 results per page)")
|
|
335
|
+
}),
|
|
336
|
+
cliMappings: {
|
|
337
|
+
options: [
|
|
338
|
+
{
|
|
339
|
+
field: "filters",
|
|
340
|
+
flags: "--filters <json>",
|
|
341
|
+
description: "JSON filter object (see docs for available filter keys)"
|
|
342
|
+
},
|
|
343
|
+
{ field: "page", flags: "--page <number>", description: "Page number (default: 1)" }
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
handler: async (input, client) => {
|
|
347
|
+
return client.post("/search-person", {
|
|
348
|
+
filters: input.filters,
|
|
349
|
+
page: input.page
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// src/commands/company/enrich.ts
|
|
355
|
+
import { z as z4 } from "zod";
|
|
356
|
+
var companyEnrichCommand = {
|
|
357
|
+
name: "company_enrich",
|
|
358
|
+
group: "company",
|
|
359
|
+
subcommand: "enrich",
|
|
360
|
+
description: "Enrich a company with 50+ data points: funding, technologies, headcount, location, job postings, social URLs, and more. Provide company_website or company_linkedin_url for best accuracy.",
|
|
361
|
+
examples: [
|
|
362
|
+
'prospeo company enrich --website "intercom.com"',
|
|
363
|
+
'prospeo company enrich --linkedin-url "https://linkedin.com/company/stripe"',
|
|
364
|
+
'prospeo company enrich --website "deloitte.com" --pretty',
|
|
365
|
+
'prospeo company enrich --company-id "cccc7c7da6116a8830a07100"',
|
|
366
|
+
'prospeo company enrich --name "Salesforce" --website "salesforce.com"'
|
|
367
|
+
],
|
|
368
|
+
inputSchema: z4.object({
|
|
369
|
+
company_website: z4.string().optional().describe("Company website domain (e.g. intercom.com)"),
|
|
370
|
+
company_linkedin_url: z4.string().url().optional().describe("Company LinkedIn URL (e.g. https://linkedin.com/company/stripe)"),
|
|
371
|
+
company_name: z4.string().optional().describe("Company name \u2014 avoid using alone, pair with website or LinkedIn URL"),
|
|
372
|
+
company_id: z4.string().optional().describe("Company ID from previous enrichment or search results")
|
|
373
|
+
}),
|
|
374
|
+
cliMappings: {
|
|
375
|
+
options: [
|
|
376
|
+
{ field: "company_website", flags: "--website <domain>", description: "Company domain (e.g. intercom.com)" },
|
|
377
|
+
{ field: "company_linkedin_url", flags: "--linkedin-url <url>", description: "Company LinkedIn URL" },
|
|
378
|
+
{ field: "company_name", flags: "--name <name>", description: "Company name (use with website or LinkedIn URL)" },
|
|
379
|
+
{ field: "company_id", flags: "--company-id <id>", description: "Company ID from previous results" }
|
|
380
|
+
]
|
|
381
|
+
},
|
|
382
|
+
handler: async (input, client) => {
|
|
383
|
+
const data = {};
|
|
384
|
+
if (input.company_website) data.company_website = input.company_website;
|
|
385
|
+
if (input.company_linkedin_url) data.company_linkedin_url = input.company_linkedin_url;
|
|
386
|
+
if (input.company_name) data.company_name = input.company_name;
|
|
387
|
+
if (input.company_id) data.company_id = input.company_id;
|
|
388
|
+
return client.post("/enrich-company", { data });
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/commands/company/bulk-enrich.ts
|
|
393
|
+
import { z as z5 } from "zod";
|
|
394
|
+
var CompanyRecord = z5.object({
|
|
395
|
+
identifier: z5.string().describe("Unique alpha-numeric string to track this record in the response"),
|
|
396
|
+
company_website: z5.string().optional(),
|
|
397
|
+
company_linkedin_url: z5.string().url().optional(),
|
|
398
|
+
company_name: z5.string().optional(),
|
|
399
|
+
company_id: z5.string().optional()
|
|
400
|
+
});
|
|
401
|
+
var companyBulkEnrichCommand = {
|
|
402
|
+
name: "company_bulk_enrich",
|
|
403
|
+
group: "company",
|
|
404
|
+
subcommand: "bulk-enrich",
|
|
405
|
+
description: 'Enrich up to 50 companies in a single request. Pass a JSON array of company objects via --data. Each object requires an "identifier" field. Returns matched[], not_matched[], and invalid_datapoints[] arrays.',
|
|
406
|
+
examples: [
|
|
407
|
+
`prospeo company bulk-enrich --data '[{"identifier":"1","company_website":"intercom.com"},{"identifier":"2","company_website":"stripe.com"}]'`,
|
|
408
|
+
`prospeo company bulk-enrich --data '[{"identifier":"a1","company_linkedin_url":"https://linkedin.com/company/salesforce"},{"identifier":"a2","company_website":"hubspot.com"}]' --pretty`
|
|
409
|
+
],
|
|
410
|
+
inputSchema: z5.object({
|
|
411
|
+
data: z5.preprocess(
|
|
412
|
+
(v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
413
|
+
z5.array(CompanyRecord).min(1).max(50)
|
|
414
|
+
).describe('JSON array of up to 50 company objects, each with a unique "identifier" field')
|
|
415
|
+
}),
|
|
416
|
+
cliMappings: {
|
|
417
|
+
options: [
|
|
418
|
+
{
|
|
419
|
+
field: "data",
|
|
420
|
+
flags: "--data <json>",
|
|
421
|
+
description: 'JSON array of company objects (each needs "identifier")'
|
|
422
|
+
}
|
|
423
|
+
]
|
|
424
|
+
},
|
|
425
|
+
handler: async (input, client) => {
|
|
426
|
+
return client.post("/bulk-enrich-company", { data: input.data });
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/commands/company/search.ts
|
|
431
|
+
import { z as z6 } from "zod";
|
|
432
|
+
var companySearchCommand = {
|
|
433
|
+
name: "company_search",
|
|
434
|
+
group: "company",
|
|
435
|
+
subcommand: "search",
|
|
436
|
+
description: "Search 30M+ companies with filters for industry, location, funding, employee count, technologies, and more. Pass filters as a JSON object via --filters. 1 credit per page returned (25 results/page).",
|
|
437
|
+
examples: [
|
|
438
|
+
`prospeo company search --filters '{"company_industry":{"include":["TECHNOLOGY"]},"company_employee_range":{"include":["51_200"]}}' --pretty`,
|
|
439
|
+
`prospeo company search --filters '{"company_location":["United States"],"company_funding":{"stage":["SERIES_A","SERIES_B"]}}' --page 1`,
|
|
440
|
+
`prospeo company search --filters '{"company":{"websites":["stripe.com","brex.com","ramp.com"]}}' --pretty`,
|
|
441
|
+
`prospeo company search --filters '{"company_industry":{"include":["FINTECH"]},"company_location":["New York"]}' --page 2`
|
|
442
|
+
],
|
|
443
|
+
inputSchema: z6.object({
|
|
444
|
+
filters: z6.preprocess(
|
|
445
|
+
(v) => typeof v === "string" ? JSON.parse(v) : v,
|
|
446
|
+
z6.record(z6.unknown())
|
|
447
|
+
).describe(
|
|
448
|
+
"JSON object of search filters. Common keys: company_industry (include/exclude), company_location (array of exact strings), company_employee_range (include/exclude), company_funding (stage/funding_date/last_funding/total_funding), company (websites array max 500, names array max 500). Use exact strings from the Prospeo dashboard for location values."
|
|
449
|
+
),
|
|
450
|
+
page: z6.coerce.number().min(1).max(1e3).default(1).describe("Page number (1\u20131000, 25 results per page)")
|
|
451
|
+
}),
|
|
452
|
+
cliMappings: {
|
|
453
|
+
options: [
|
|
454
|
+
{
|
|
455
|
+
field: "filters",
|
|
456
|
+
flags: "--filters <json>",
|
|
457
|
+
description: "JSON filter object"
|
|
458
|
+
},
|
|
459
|
+
{ field: "page", flags: "--page <number>", description: "Page number (default: 1)" }
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
handler: async (input, client) => {
|
|
463
|
+
return client.post("/search-company", {
|
|
464
|
+
filters: input.filters,
|
|
465
|
+
page: input.page
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/commands/suggestions/location.ts
|
|
471
|
+
import { z as z7 } from "zod";
|
|
472
|
+
var suggestionsLocationCommand = {
|
|
473
|
+
name: "suggestions_location",
|
|
474
|
+
group: "suggestions",
|
|
475
|
+
subcommand: "location",
|
|
476
|
+
description: 'Get autocomplete location suggestions for use in person/company search filters. Returns COUNTRY, STATE, CITY, and ZONE entries. Free \u2014 no credits consumed. Use the returned "name" values in company_location and person_location filter arrays.',
|
|
477
|
+
examples: [
|
|
478
|
+
'prospeo suggestions location --query "united states"',
|
|
479
|
+
'prospeo suggestions location --query "new york" --pretty',
|
|
480
|
+
'prospeo suggestions location --query "san fra"',
|
|
481
|
+
'prospeo suggestions location --query "greater toronto"'
|
|
482
|
+
],
|
|
483
|
+
inputSchema: z7.object({
|
|
484
|
+
query: z7.string().min(2).describe('Location search query (minimum 2 characters). E.g. "united states", "california", "new york"')
|
|
485
|
+
}),
|
|
486
|
+
cliMappings: {
|
|
487
|
+
options: [
|
|
488
|
+
{
|
|
489
|
+
field: "query",
|
|
490
|
+
flags: "--query <text>",
|
|
491
|
+
description: "Location search query (min 2 chars)"
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
handler: async (input, client) => {
|
|
496
|
+
return client.post("/search-suggestions", {
|
|
497
|
+
location_search: input.query
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/commands/suggestions/job-title.ts
|
|
503
|
+
import { z as z8 } from "zod";
|
|
504
|
+
var suggestionsJobTitleCommand = {
|
|
505
|
+
name: "suggestions_job_title",
|
|
506
|
+
group: "suggestions",
|
|
507
|
+
subcommand: "job-title",
|
|
508
|
+
description: "Get autocomplete job title suggestions for use in person search filters. Returns up to 25 results ordered by popularity. Free \u2014 no credits consumed. Use the returned values in the person_job_title filter array.",
|
|
509
|
+
examples: [
|
|
510
|
+
'prospeo suggestions job-title --query "head of sales"',
|
|
511
|
+
'prospeo suggestions job-title --query "vp engineering" --pretty',
|
|
512
|
+
'prospeo suggestions job-title --query "chief revenue"',
|
|
513
|
+
'prospeo suggestions job-title --query "software engineer"'
|
|
514
|
+
],
|
|
515
|
+
inputSchema: z8.object({
|
|
516
|
+
query: z8.string().min(2).describe('Job title search query (minimum 2 characters). E.g. "head of sales", "vp engineer"')
|
|
517
|
+
}),
|
|
518
|
+
cliMappings: {
|
|
519
|
+
options: [
|
|
520
|
+
{
|
|
521
|
+
field: "query",
|
|
522
|
+
flags: "--query <text>",
|
|
523
|
+
description: "Job title search query (min 2 chars)"
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
},
|
|
527
|
+
handler: async (input, client) => {
|
|
528
|
+
return client.post("/search-suggestions", {
|
|
529
|
+
job_title_search: input.query
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/commands/account/info.ts
|
|
535
|
+
import { z as z9 } from "zod";
|
|
536
|
+
var accountInfoCommand = {
|
|
537
|
+
name: "account_info",
|
|
538
|
+
group: "account",
|
|
539
|
+
subcommand: "info",
|
|
540
|
+
description: "Get account information: current plan, remaining credits, used credits, team members, and next quota renewal date. Free \u2014 no credits consumed.",
|
|
541
|
+
examples: [
|
|
542
|
+
"prospeo account info",
|
|
543
|
+
"prospeo account info --pretty",
|
|
544
|
+
"prospeo account info --fields remaining_credits,next_quota_renewal_date"
|
|
545
|
+
],
|
|
546
|
+
inputSchema: z9.object({}),
|
|
547
|
+
cliMappings: {},
|
|
548
|
+
handler: async (_input, client) => {
|
|
549
|
+
return client.get("/account-information");
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// src/commands/index.ts
|
|
554
|
+
var allCommands = [
|
|
555
|
+
// Person (3)
|
|
556
|
+
personEnrichCommand,
|
|
557
|
+
personBulkEnrichCommand,
|
|
558
|
+
personSearchCommand,
|
|
559
|
+
// Company (3)
|
|
560
|
+
companyEnrichCommand,
|
|
561
|
+
companyBulkEnrichCommand,
|
|
562
|
+
companySearchCommand,
|
|
563
|
+
// Suggestions (2)
|
|
564
|
+
suggestionsLocationCommand,
|
|
565
|
+
suggestionsJobTitleCommand,
|
|
566
|
+
// Account (1)
|
|
567
|
+
accountInfoCommand
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
// src/mcp/server.ts
|
|
571
|
+
async function startMcpServer() {
|
|
572
|
+
const apiKey = await resolveApiKey();
|
|
573
|
+
const client = new ProspeoClient({ apiKey });
|
|
574
|
+
const server = new McpServer({
|
|
575
|
+
name: "prospeo",
|
|
576
|
+
version: "0.1.0"
|
|
577
|
+
});
|
|
578
|
+
for (const cmdDef of allCommands) {
|
|
579
|
+
const shape = cmdDef.inputSchema.shape;
|
|
580
|
+
server.registerTool(
|
|
581
|
+
cmdDef.name,
|
|
582
|
+
{
|
|
583
|
+
description: cmdDef.description,
|
|
584
|
+
inputSchema: shape
|
|
585
|
+
},
|
|
586
|
+
async (args) => {
|
|
587
|
+
try {
|
|
588
|
+
const result = await cmdDef.handler(args, client);
|
|
589
|
+
return {
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: "text",
|
|
593
|
+
text: JSON.stringify(result, null, 2)
|
|
594
|
+
}
|
|
595
|
+
]
|
|
596
|
+
};
|
|
597
|
+
} catch (error) {
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: JSON.stringify({
|
|
603
|
+
error: error.message ?? String(error),
|
|
604
|
+
code: error.code ?? "UNKNOWN_ERROR"
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
isError: true
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const transport = new StdioServerTransport();
|
|
615
|
+
await server.connect(transport);
|
|
616
|
+
console.error("Prospeo MCP server started. Tools registered:", allCommands.length);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/mcp.ts
|
|
620
|
+
process.on("SIGINT", () => process.exit(0));
|
|
621
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
622
|
+
startMcpServer().catch((error) => {
|
|
623
|
+
console.error("Failed to start MCP server:", error.message ?? error);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
});
|
|
626
|
+
//# sourceMappingURL=mcp.js.map
|