@sellable/mcp 0.1.268 → 0.1.269

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.
@@ -0,0 +1,429 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { getApi } from "../api.js";
5
+ import { resolveWorkspaceRoot } from "../utils/workspace-root.js";
6
+ const entryPath = process.argv[1] ? path.resolve(process.argv[1]) : process.cwd();
7
+ const workspaceRoot = resolveWorkspaceRoot(path.dirname(entryPath));
8
+ const HARVEST_JOB_SEARCH_TOKEN_REF_PREFIX = "mcp-harvest-job-search-token:";
9
+ const MAX_HARVEST_JOB_SEARCH_TOKEN_REFS = 200;
10
+ const tokenRefs = new Map();
11
+ let tokenRefCounter = 0;
12
+ export const harvestJobToolDefinitions = [
13
+ {
14
+ name: "search_harvest_jobs",
15
+ description: "Search current LinkedIn jobs through Sellable's Harvest-backed v3 API, write markdown/CSV review artifacts, and return a token reference for selected-company confirmation. Use when account-source intent is current LinkedIn job posts. Does not call Prospeo or confirm companies.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ search: { type: "string" },
20
+ searches: { type: "array", items: { type: "string" } },
21
+ location: { type: "string" },
22
+ geoId: { type: "string" },
23
+ postedLimit: { type: "string", enum: ["24h", "week", "month"] },
24
+ sortBy: { type: "string", enum: ["relevance", "date"] },
25
+ workplaceType: {
26
+ oneOf: [
27
+ { type: "string" },
28
+ { type: "array", items: { type: "string" } },
29
+ ],
30
+ },
31
+ employmentType: {
32
+ oneOf: [
33
+ { type: "string" },
34
+ { type: "array", items: { type: "string" } },
35
+ ],
36
+ },
37
+ experienceLevel: {
38
+ oneOf: [
39
+ { type: "string" },
40
+ { type: "array", items: { type: "string" } },
41
+ ],
42
+ },
43
+ easyApply: { type: "boolean" },
44
+ under10Applicants: { type: "boolean" },
45
+ salary: { type: "string" },
46
+ pages: {
47
+ type: "number",
48
+ description: "Max pages per search term. Backend cap is 2.",
49
+ },
50
+ maxRows: {
51
+ type: "number",
52
+ description: "Max returned rows across searches. Backend cap is 100.",
53
+ },
54
+ artifactFormat: {
55
+ type: "string",
56
+ enum: ["markdown", "csv", "both"],
57
+ description: "Review artifact format. Defaults to markdown.",
58
+ },
59
+ outputDir: {
60
+ type: "string",
61
+ description: "Optional safe output directory under the workspace, home, or temp root.",
62
+ },
63
+ fileBaseName: {
64
+ type: "string",
65
+ description: "Optional safe artifact filename base without extension.",
66
+ },
67
+ },
68
+ required: [],
69
+ additionalProperties: false,
70
+ },
71
+ },
72
+ {
73
+ name: "confirm_harvest_job_companies",
74
+ description: "Confirm selected Harvest LinkedIn job IDs into a backend-resolved Prospeo domainFilterId, then guide the follow-on search_prospeo people search. Requires searchToken or token reference from search_harvest_jobs. Does not accept raw domains.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ searchToken: {
79
+ type: "string",
80
+ description: "Signed search token or mcp-harvest-job-search-token:* reference from search_harvest_jobs.",
81
+ },
82
+ selectedJobIds: {
83
+ type: "array",
84
+ items: { type: "string" },
85
+ description: "Harvest job IDs selected from the search artifact.",
86
+ },
87
+ name: { type: "string" },
88
+ outputDir: {
89
+ type: "string",
90
+ description: "Optional safe output directory under the workspace, home, or temp root.",
91
+ },
92
+ fileBaseName: {
93
+ type: "string",
94
+ description: "Optional safe artifact filename base without extension.",
95
+ },
96
+ },
97
+ required: ["searchToken", "selectedJobIds"],
98
+ additionalProperties: false,
99
+ },
100
+ },
101
+ ];
102
+ function removeUndefinedValues(input) {
103
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
104
+ }
105
+ function storeTokenRef(token) {
106
+ if (typeof token !== "string" || token.length === 0)
107
+ return token;
108
+ if (token.startsWith(HARVEST_JOB_SEARCH_TOKEN_REF_PREFIX))
109
+ return token;
110
+ const ref = `${HARVEST_JOB_SEARCH_TOKEN_REF_PREFIX}${Date.now().toString(36)}-${(++tokenRefCounter).toString(36)}`;
111
+ tokenRefs.set(ref, token);
112
+ while (tokenRefs.size > MAX_HARVEST_JOB_SEARCH_TOKEN_REFS) {
113
+ const oldest = tokenRefs.keys().next().value;
114
+ if (!oldest)
115
+ break;
116
+ tokenRefs.delete(oldest);
117
+ }
118
+ return ref;
119
+ }
120
+ function resolveTokenRef(token) {
121
+ if (!token.startsWith(HARVEST_JOB_SEARCH_TOKEN_REF_PREFIX))
122
+ return token;
123
+ const resolved = tokenRefs.get(token);
124
+ if (!resolved) {
125
+ throw new Error("confirm_harvest_job_companies received a stale Harvest job search token reference. Re-run search_harvest_jobs.");
126
+ }
127
+ return resolved;
128
+ }
129
+ function validateSearchInput(input) {
130
+ if (!input?.search && (!input?.searches || input.searches.length === 0)) {
131
+ throw new Error("search_harvest_jobs requires search or searches.");
132
+ }
133
+ }
134
+ function validateConfirmInput(input) {
135
+ if (!input?.searchToken) {
136
+ throw new Error("confirm_harvest_job_companies requires searchToken from search_harvest_jobs.");
137
+ }
138
+ if (!input.selectedJobIds || input.selectedJobIds.length === 0) {
139
+ throw new Error("confirm_harvest_job_companies requires selectedJobIds from search_harvest_jobs results.");
140
+ }
141
+ if ("domain" in input ||
142
+ "domains" in input ||
143
+ "includeDomains" in input ||
144
+ "companyDomains" in input) {
145
+ throw new Error("confirm_harvest_job_companies does not accept raw domains. Use searchToken and selectedJobIds.");
146
+ }
147
+ }
148
+ function decodeMaybe(value) {
149
+ try {
150
+ return decodeURIComponent(value);
151
+ }
152
+ catch {
153
+ return value;
154
+ }
155
+ }
156
+ function isPathInside(candidate, root) {
157
+ const relative = path.relative(root, candidate);
158
+ return (relative === "" ||
159
+ (!relative.startsWith("..") && !path.isAbsolute(relative)));
160
+ }
161
+ function safeBaseName(value, fallback) {
162
+ const raw = (value ?? fallback).trim();
163
+ const decoded = decodeMaybe(raw);
164
+ const cleaned = decoded
165
+ .replace(/\.[A-Za-z0-9]+$/, "")
166
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
167
+ .replace(/^-+|-+$/g, "")
168
+ .slice(0, 80);
169
+ if (!cleaned || cleaned.includes("..")) {
170
+ throw new Error("fileBaseName must be a safe filename.");
171
+ }
172
+ return cleaned;
173
+ }
174
+ function allowedRoots() {
175
+ return [workspaceRoot, os.homedir(), os.tmpdir()]
176
+ .map((root) => {
177
+ const resolved = path.resolve(root);
178
+ try {
179
+ return fs.realpathSync(resolved);
180
+ }
181
+ catch {
182
+ return resolved;
183
+ }
184
+ })
185
+ .filter(Boolean);
186
+ }
187
+ function resolveOutputDir(outputDir) {
188
+ const target = outputDir
189
+ ? path.isAbsolute(outputDir)
190
+ ? path.resolve(outputDir)
191
+ : path.resolve(workspaceRoot, outputDir)
192
+ : path.resolve(workspaceRoot, ".sellable", "artifacts", "harvest-jobs");
193
+ if (target.includes("\0")) {
194
+ throw new Error("outputDir contains invalid characters.");
195
+ }
196
+ fs.mkdirSync(target, { recursive: true });
197
+ const real = fs.realpathSync(target);
198
+ if (!allowedRoots().some((root) => isPathInside(real, root))) {
199
+ throw new Error("outputDir must be inside the workspace, home directory, or temp directory.");
200
+ }
201
+ return real;
202
+ }
203
+ function collisionSafePath(outputDir, fileBaseName, ext) {
204
+ const extension = ext.startsWith(".") ? ext : `.${ext}`;
205
+ if (![".md", ".csv"].includes(extension)) {
206
+ throw new Error(`Unsupported artifact extension: ${extension}`);
207
+ }
208
+ let candidate = path.join(outputDir, `${fileBaseName}${extension}`);
209
+ let counter = 1;
210
+ while (fs.existsSync(candidate)) {
211
+ candidate = path.join(outputDir, `${fileBaseName}-${counter}${extension}`);
212
+ counter += 1;
213
+ }
214
+ const resolved = path.resolve(candidate);
215
+ if (!isPathInside(resolved, outputDir)) {
216
+ throw new Error("Artifact path escapes outputDir.");
217
+ }
218
+ if (fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
219
+ throw new Error("Refusing to write through symlink artifact path.");
220
+ }
221
+ return resolved;
222
+ }
223
+ function csvEscape(value) {
224
+ const text = value === null || value === undefined ? "" : String(value);
225
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
226
+ }
227
+ function formatValue(value) {
228
+ if (value === null || value === undefined)
229
+ return "";
230
+ if (typeof value === "object")
231
+ return JSON.stringify(value);
232
+ return String(value);
233
+ }
234
+ function renderSearchMarkdown(response) {
235
+ const rows = Array.isArray(response.rows) ? response.rows : [];
236
+ const lines = [
237
+ "# Harvest LinkedIn Job Search",
238
+ "",
239
+ `Row count: ${response.rowCount ?? rows.length}`,
240
+ `Cost summary: ${JSON.stringify(response.costSummary ?? {})}`,
241
+ `Search summary: ${JSON.stringify(response.summary ?? {})}`,
242
+ "",
243
+ "Domains are resolved only during confirmation. Select stable Harvest job IDs, not LinkedIn company URLs.",
244
+ "",
245
+ "| Job ID | Company | Title | Posted | Location | Easy Apply | Job URL | Company LinkedIn | Request ID |",
246
+ "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
247
+ ...rows.map((row) => [
248
+ row.jobId,
249
+ row.companyName,
250
+ row.title,
251
+ row.postedDate,
252
+ row.locationText,
253
+ row.easyApply,
254
+ row.jobUrl,
255
+ row.companyLinkedinUrl,
256
+ row.harvestRequestId,
257
+ ]
258
+ .map((value) => String(value ?? "").replace(/\|/g, "\\|"))
259
+ .join(" | ")
260
+ .replace(/^/, "| ")
261
+ .replace(/$/, " |")),
262
+ "",
263
+ "Next step: confirm_harvest_job_companies({ searchToken, selectedJobIds: [...] })",
264
+ ];
265
+ return `${lines.join("\n")}\n`;
266
+ }
267
+ function renderSearchCsv(response) {
268
+ const rows = Array.isArray(response.rows) ? response.rows : [];
269
+ const columns = [
270
+ "rowId",
271
+ "jobId",
272
+ "companyName",
273
+ "title",
274
+ "jobUrl",
275
+ "postedDate",
276
+ "locationText",
277
+ "easyApply",
278
+ "companyLinkedinUrl",
279
+ "companyUniversalName",
280
+ "harvestRequestId",
281
+ "search",
282
+ "page",
283
+ ];
284
+ return [
285
+ columns.join(","),
286
+ ...rows.map((row) => columns.map((column) => csvEscape(row[column])).join(",")),
287
+ ].join("\n");
288
+ }
289
+ function renderConfirmMarkdown(response) {
290
+ const unresolved = Array.isArray(response.unresolvedJobs)
291
+ ? response.unresolvedJobs
292
+ : [];
293
+ const skipped = Array.isArray(response.skippedJobs) ? response.skippedJobs : [];
294
+ const lines = [
295
+ "# Harvest LinkedIn Job Confirmation",
296
+ "",
297
+ `Domain filter ID: ${response.domainFilterId ?? ""}`,
298
+ `Resolved: ${response.resolvedCount ?? 0}`,
299
+ `Unresolved: ${response.unresolvedCount ?? 0}`,
300
+ `Duplicates: ${response.duplicateCount ?? 0}`,
301
+ `Skipped: ${response.skippedCount ?? 0}`,
302
+ `Cost summary: ${JSON.stringify(response.costSummary ?? {})}`,
303
+ "",
304
+ "## Resolved Domains",
305
+ "",
306
+ ...(response.includeDomains ?? []).map((domain) => `- ${domain}`),
307
+ "",
308
+ "## Unresolved Jobs",
309
+ "",
310
+ ...unresolved.map((row) => `- ${formatValue(row.jobId)}: ${formatValue(row.reason)}`),
311
+ "",
312
+ "## Skipped Jobs",
313
+ "",
314
+ ...skipped.map((row) => `- ${formatValue(row.jobId)}: ${formatValue(row.reason)}`),
315
+ "",
316
+ "Next step: search_prospeo with the returned domainFilterId.",
317
+ ];
318
+ return `${lines.join("\n")}\n`;
319
+ }
320
+ function writeSearchArtifacts(response, input) {
321
+ const format = input.artifactFormat ?? "markdown";
322
+ const outputDir = resolveOutputDir(input.outputDir);
323
+ const baseName = safeBaseName(input.fileBaseName, `harvest-jobs-${new Date().toISOString().replace(/[:.]/g, "-")}`);
324
+ const artifacts = {};
325
+ if (format === "markdown" || format === "both") {
326
+ const markdownPath = collisionSafePath(outputDir, baseName, ".md");
327
+ fs.writeFileSync(markdownPath, renderSearchMarkdown(response), "utf8");
328
+ artifacts.markdown = markdownPath;
329
+ }
330
+ if (format === "csv" || format === "both") {
331
+ const csvPath = collisionSafePath(outputDir, baseName, ".csv");
332
+ fs.writeFileSync(csvPath, renderSearchCsv(response), "utf8");
333
+ artifacts.csv = csvPath;
334
+ }
335
+ return artifacts;
336
+ }
337
+ function writeConfirmArtifact(response, input) {
338
+ const outputDir = resolveOutputDir(input.outputDir);
339
+ const baseName = safeBaseName(input.fileBaseName, `harvest-jobs-confirm-${new Date().toISOString().replace(/[:.]/g, "-")}`);
340
+ const artifactPath = collisionSafePath(outputDir, baseName, ".md");
341
+ fs.writeFileSync(artifactPath, renderConfirmMarkdown(response), "utf8");
342
+ return artifactPath;
343
+ }
344
+ function compactSearchResponse(response, artifacts) {
345
+ const rows = Array.isArray(response.rows) ? response.rows : [];
346
+ const token = storeTokenRef(response.searchToken);
347
+ return removeUndefinedValues({
348
+ success: response.success ?? true,
349
+ rowCount: typeof response.rowCount === "number" ? response.rowCount : rows.length,
350
+ costSummary: response.costSummary ?? {},
351
+ artifacts,
352
+ sampleJobs: rows.slice(0, 10).map((row) => removeUndefinedValues({
353
+ rowId: row.rowId,
354
+ jobId: row.jobId,
355
+ company: row.companyName,
356
+ title: row.title,
357
+ jobUrl: row.jobUrl,
358
+ postedDate: row.postedDate,
359
+ location: row.locationText,
360
+ easyApply: row.easyApply,
361
+ companyLinkedinUrl: row.companyLinkedinUrl,
362
+ companyUniversalName: row.companyUniversalName,
363
+ })),
364
+ searchToken: token,
365
+ nextStep: "Review the artifact, then call confirm_harvest_job_companies with searchToken and selectedJobIds. Harvest jobs are account-source evidence; Prospeo is the follow-on people search.",
366
+ });
367
+ }
368
+ function compactConfirmResponse(response, artifactPath) {
369
+ return removeUndefinedValues({
370
+ success: response.success ?? true,
371
+ domainFilterId: response.domainFilterId,
372
+ includeDomains: response.includeDomains,
373
+ resolvedCount: response.resolvedCount,
374
+ unresolvedCount: response.unresolvedCount,
375
+ duplicateCount: response.duplicateCount,
376
+ skippedCount: response.skippedCount,
377
+ costSummary: response.costSummary,
378
+ unresolvedJobs: response.unresolvedJobs ?? [],
379
+ skippedJobs: response.skippedJobs ?? [],
380
+ carryoverSummary: response.carryoverSummary,
381
+ confirmArtifactPath: artifactPath,
382
+ suggestedNextCall: {
383
+ tool: "search_prospeo",
384
+ arguments: response.nextSearchProspeoCall ?? {
385
+ domainFilterId: response.domainFilterId,
386
+ filters: { max_person_per_company: 1 },
387
+ },
388
+ },
389
+ });
390
+ }
391
+ export async function searchHarvestJobs(input) {
392
+ validateSearchInput(input);
393
+ const api = getApi();
394
+ const response = await api.post("/api/v3/harvest/linkedin-jobs/search", removeUndefinedValues({
395
+ search: input.search,
396
+ searches: input.searches,
397
+ location: input.location,
398
+ geoId: input.geoId,
399
+ postedLimit: input.postedLimit,
400
+ sortBy: input.sortBy,
401
+ workplaceType: input.workplaceType,
402
+ employmentType: input.employmentType,
403
+ experienceLevel: input.experienceLevel,
404
+ easyApply: input.easyApply,
405
+ under10Applicants: input.under10Applicants,
406
+ salary: input.salary,
407
+ pages: input.pages,
408
+ maxRows: input.maxRows,
409
+ }));
410
+ if (!response || typeof response !== "object" || !response.searchToken) {
411
+ throw new Error("search_harvest_jobs API response did not include a searchToken.");
412
+ }
413
+ const artifacts = writeSearchArtifacts(response, input);
414
+ return compactSearchResponse(response, artifacts);
415
+ }
416
+ export async function confirmHarvestJobCompanies(input) {
417
+ validateConfirmInput(input);
418
+ const api = getApi();
419
+ const response = await api.post("/api/v3/harvest/linkedin-jobs/confirm-domain-filter", removeUndefinedValues({
420
+ searchToken: resolveTokenRef(input.searchToken),
421
+ selectedJobIds: input.selectedJobIds,
422
+ name: input.name,
423
+ }));
424
+ if (!response || typeof response !== "object" || !response.domainFilterId) {
425
+ throw new Error("confirm_harvest_job_companies API response did not include a domainFilterId.");
426
+ }
427
+ const artifactPath = writeConfirmArtifact(response, input);
428
+ return compactConfirmResponse(response, artifactPath);
429
+ }
@@ -1813,7 +1813,7 @@ export const leadToolDefinitions = [
1813
1813
  },
1814
1814
  {
1815
1815
  name: "search_prospeo",
1816
- description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. If output includes warnings/fallback, report that the MCP retried a rejected precise title request with a safer domain-filter people search instead of claiming the original precise filter succeeded. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1816
+ description: 'Search Prospeo for people using filters and optional domainFilterId. Requires get_provider_prompt({ provider: "prospeo" }) first. Use Prospeo first for hiring-led targeting because it supports company_job_posting_hiring_for and company_job_posting_quantity; Sales Nav does not filter companies by hiring role. When targeting known accounts, call load_csv_domains for CSV-on-disk workflows or save_domain_filters for pasted/raw domain lists, then pass domainFilterId. For current LinkedIn job-post account-source asks, call search_harvest_jobs first, review the artifact, then confirm_harvest_job_companies to create a domainFilterId before using search_prospeo for people. For company lookalike/account asks, call search_prospeo_companies first, review accounts, then confirm_prospeo_company_accounts to create a domainFilterId before using search_prospeo for people. Raw domain inputs, LinkedIn company URLs as domains, and company-name targeting are NOT supported in this MCP tool. Strategy: start with 2-3 high-signal filters (title/seniority + industry or domainFilterId + headcount), add job-posting filters for hiring-led campaigns, then tighten one filter at a time. For security, AppSec, SOC, RevOps, Demand Gen, and similar function-specific lanes, do not widen with bare seniority labels like "Head" or "Director" alone; pair them with explicit function-title keywords and inspect the sample for off-function `Head of X` leakage. If output includes warnings/fallback, report that the MCP retried a rejected precise title request with a safer domain-filter people search instead of claiming the original precise filter succeeded. Prefer person location over company HQ unless HQ is explicitly needed. `campaignOfferId` routing rule: OMIT campaignOfferId ONLY in pre-mint Phase 84 `find leads` discovery mode (validating ICP before the commit gate). In every other context — post-mint lead additions, operator-driven searches on a live campaign, any search where the intent is to persist results to a specific campaign — you MUST pass campaignOfferId so the search shows up in that campaign\'s Contact Search panel. Post-mint create-campaign-v2 watch runs MUST pass currentStep: "prospeo". Omitting campaignOfferId post-mint orphans the search from the UI. Returns normalized results with pagination.',
1817
1817
  inputSchema: {
1818
1818
  type: "object",
1819
1819
  properties: {
@@ -1260,19 +1260,6 @@ export declare const allTools: ({
1260
1260
  body?: undefined;
1261
1261
  validationReceipt?: undefined;
1262
1262
  status?: undefined;
1263
- postText?: undefined;
1264
- sourceLabel?: undefined;
1265
- measurementMode?: undefined;
1266
- requireBrowserMeasurement?: undefined;
1267
- clampLines?: undefined;
1268
- mobileClampLines?: undefined;
1269
- desktopClampLines?: undefined;
1270
- mobileTextWidthPx?: undefined;
1271
- desktopTextWidthPx?: undefined;
1272
- renderId?: undefined;
1273
- renderScreenshots?: undefined;
1274
- requireScreenshots?: undefined;
1275
- reviewClampLines?: undefined;
1276
1263
  updatedAt?: undefined;
1277
1264
  publishUrl?: undefined;
1278
1265
  activityId?: undefined;
@@ -1308,7 +1295,6 @@ export declare const allTools: ({
1308
1295
  properties: {
1309
1296
  draftId: {
1310
1297
  type: string;
1311
- description?: undefined;
1312
1298
  };
1313
1299
  ideaId: {
1314
1300
  type: string;
@@ -1353,19 +1339,6 @@ export declare const allTools: ({
1353
1339
  selectedPatterns?: undefined;
1354
1340
  previewBudget?: undefined;
1355
1341
  notes?: undefined;
1356
- postText?: undefined;
1357
- sourceLabel?: undefined;
1358
- measurementMode?: undefined;
1359
- requireBrowserMeasurement?: undefined;
1360
- clampLines?: undefined;
1361
- mobileClampLines?: undefined;
1362
- desktopClampLines?: undefined;
1363
- mobileTextWidthPx?: undefined;
1364
- desktopTextWidthPx?: undefined;
1365
- renderId?: undefined;
1366
- renderScreenshots?: undefined;
1367
- requireScreenshots?: undefined;
1368
- reviewClampLines?: undefined;
1369
1342
  updatedAt?: undefined;
1370
1343
  publishUrl?: undefined;
1371
1344
  activityId?: undefined;
@@ -1388,7 +1361,6 @@ export declare const allTools: ({
1388
1361
  properties: {
1389
1362
  draftId: {
1390
1363
  type: string;
1391
- description?: undefined;
1392
1364
  };
1393
1365
  hookResearchId: {
1394
1366
  type: string;
@@ -1431,19 +1403,6 @@ export declare const allTools: ({
1431
1403
  previewBudget?: undefined;
1432
1404
  notes?: undefined;
1433
1405
  createdAt?: undefined;
1434
- postText?: undefined;
1435
- sourceLabel?: undefined;
1436
- measurementMode?: undefined;
1437
- requireBrowserMeasurement?: undefined;
1438
- clampLines?: undefined;
1439
- mobileClampLines?: undefined;
1440
- desktopClampLines?: undefined;
1441
- mobileTextWidthPx?: undefined;
1442
- desktopTextWidthPx?: undefined;
1443
- renderId?: undefined;
1444
- renderScreenshots?: undefined;
1445
- requireScreenshots?: undefined;
1446
- reviewClampLines?: undefined;
1447
1406
  publishUrl?: undefined;
1448
1407
  activityId?: undefined;
1449
1408
  publishedAt?: undefined;
@@ -1465,7 +1424,6 @@ export declare const allTools: ({
1465
1424
  properties: {
1466
1425
  draftId: {
1467
1426
  type: string;
1468
- description?: undefined;
1469
1427
  };
1470
1428
  publishUrl: {
1471
1429
  type: string;
@@ -1506,19 +1464,6 @@ export declare const allTools: ({
1506
1464
  body?: undefined;
1507
1465
  validationReceipt?: undefined;
1508
1466
  status?: undefined;
1509
- postText?: undefined;
1510
- sourceLabel?: undefined;
1511
- measurementMode?: undefined;
1512
- requireBrowserMeasurement?: undefined;
1513
- clampLines?: undefined;
1514
- mobileClampLines?: undefined;
1515
- desktopClampLines?: undefined;
1516
- mobileTextWidthPx?: undefined;
1517
- desktopTextWidthPx?: undefined;
1518
- renderId?: undefined;
1519
- renderScreenshots?: undefined;
1520
- requireScreenshots?: undefined;
1521
- reviewClampLines?: undefined;
1522
1467
  updatedAt?: undefined;
1523
1468
  publishedPostId?: undefined;
1524
1469
  year?: undefined;
@@ -1571,19 +1516,6 @@ export declare const allTools: ({
1571
1516
  body?: undefined;
1572
1517
  validationReceipt?: undefined;
1573
1518
  status?: undefined;
1574
- postText?: undefined;
1575
- sourceLabel?: undefined;
1576
- measurementMode?: undefined;
1577
- requireBrowserMeasurement?: undefined;
1578
- clampLines?: undefined;
1579
- mobileClampLines?: undefined;
1580
- desktopClampLines?: undefined;
1581
- mobileTextWidthPx?: undefined;
1582
- desktopTextWidthPx?: undefined;
1583
- renderId?: undefined;
1584
- renderScreenshots?: undefined;
1585
- requireScreenshots?: undefined;
1586
- reviewClampLines?: undefined;
1587
1519
  updatedAt?: undefined;
1588
1520
  publishUrl?: undefined;
1589
1521
  activityId?: undefined;
@@ -1817,14 +1749,6 @@ export declare const allTools: ({
1817
1749
  };
1818
1750
  description: string;
1819
1751
  };
1820
- targetFollowerMin: {
1821
- type: string;
1822
- description: string;
1823
- };
1824
- targetFollowerMax: {
1825
- type: string;
1826
- description: string;
1827
- };
1828
1752
  };
1829
1753
  required: string[];
1830
1754
  additionalProperties: boolean;
@@ -1956,6 +1880,53 @@ export declare const allTools: ({
1956
1880
  };
1957
1881
  required: never[];
1958
1882
  };
1883
+ } | {
1884
+ name: string;
1885
+ description: string;
1886
+ inputSchema: {
1887
+ type: string;
1888
+ properties: {
1889
+ searchToken: {
1890
+ type: string;
1891
+ description: string;
1892
+ };
1893
+ selectedJobIds: {
1894
+ type: string;
1895
+ items: {
1896
+ type: string;
1897
+ };
1898
+ description: string;
1899
+ };
1900
+ name: {
1901
+ type: string;
1902
+ };
1903
+ outputDir: {
1904
+ type: string;
1905
+ description: string;
1906
+ };
1907
+ fileBaseName: {
1908
+ type: string;
1909
+ description: string;
1910
+ };
1911
+ search?: undefined;
1912
+ searches?: undefined;
1913
+ location?: undefined;
1914
+ geoId?: undefined;
1915
+ postedLimit?: undefined;
1916
+ sortBy?: undefined;
1917
+ workplaceType?: undefined;
1918
+ employmentType?: undefined;
1919
+ experienceLevel?: undefined;
1920
+ easyApply?: undefined;
1921
+ under10Applicants?: undefined;
1922
+ salary?: undefined;
1923
+ pages?: undefined;
1924
+ maxRows?: undefined;
1925
+ artifactFormat?: undefined;
1926
+ };
1927
+ required: string[];
1928
+ additionalProperties: boolean;
1929
+ };
1959
1930
  } | {
1960
1931
  name: string;
1961
1932
  description: string;
@@ -14,6 +14,7 @@ import { engageMemoryToolDefinitions } from "./engage-memory.js";
14
14
  import { engageStateToolDefinitions } from "./engage-state.js";
15
15
  import { enrichmentToolDefinitions } from "./enrichment.js";
16
16
  import { frameworkToolDefinitions } from "./framework.js";
17
+ import { harvestJobToolDefinitions } from "./harvest-jobs.js";
17
18
  import { leadToolDefinitions } from "./leads.js";
18
19
  import { linkedinToolDefinitions } from "./linkedin.js";
19
20
  import { navigationToolDefinitions } from "./navigation.js";
@@ -43,6 +44,7 @@ export const allTools = [
43
44
  ...contentPostToolDefinitions,
44
45
  ...navigationToolDefinitions,
45
46
  ...leadToolDefinitions,
47
+ ...harvestJobToolDefinitions,
46
48
  ...enrichmentToolDefinitions,
47
49
  ...processingToolDefinitions,
48
50
  ...rubricToolDefinitions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.268",
3
+ "version": "0.1.269",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -29,6 +29,8 @@ allowed-tools:
29
29
  - mcp__sellable__search_prospeo
30
30
  - mcp__sellable__search_prospeo_companies
31
31
  - mcp__sellable__confirm_prospeo_company_accounts
32
+ - mcp__sellable__search_harvest_jobs
33
+ - mcp__sellable__confirm_harvest_job_companies
32
34
  - mcp__sellable__search_signals
33
35
  - mcp__sellable__fetch_post_engagers
34
36
  - mcp__sellable__enrich_with_prospeo
@@ -227,6 +229,14 @@ are likely. Sales Nav is useful for recent LinkedIn activity, role/title
227
229
  precision, and referral paths, but it does not provide hiring-by-role filters;
228
230
  say that distinction plainly in the source-plan gate.
229
231
 
232
+ When the brief asks for current LinkedIn job-post intent, such as companies
233
+ hiring Power BI developers this month, use Harvest jobs as the account source:
234
+ `search_harvest_jobs -> confirm_harvest_job_companies -> search_prospeo`.
235
+ First write and review the Harvest job artifact. Then confirm selected Harvest
236
+ job IDs into a `domainFilterId`; Prospeo remains the people-search provider.
237
+ Do not paste LinkedIn company URLs as domains. Do not fetch full job details for
238
+ every search row by default; selected batches only.
239
+
230
240
  For company lookalikes, best-customer lookalikes, "companies like X",
231
241
  lookalike accounts, companies that use AI, companies with API/SSO/Chrome
232
242
  extension, news/award/integration/key-customer filters, or account discovery