@sellable/mcp 0.1.255 → 0.1.256

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/api.d.ts CHANGED
@@ -10,8 +10,11 @@ export declare class SellableApiError extends Error {
10
10
  constructor(status: number, body: string, guidance?: string);
11
11
  }
12
12
  export declare class SellableApi {
13
+ private buildError;
14
+ private requestResponse;
13
15
  private request;
14
16
  get<T>(path: string): Promise<T>;
17
+ getText(path: string): Promise<string>;
15
18
  post<T>(path: string, body?: object): Promise<T>;
16
19
  put<T>(path: string, body?: object): Promise<T>;
17
20
  patch<T>(path: string, body?: object): Promise<T>;
package/dist/api.js CHANGED
@@ -14,7 +14,21 @@ export class SellableApiError extends Error {
14
14
  }
15
15
  }
16
16
  export class SellableApi {
17
- async request(method, path, body) {
17
+ buildError(status, errorText) {
18
+ const isAuthError = status === 401 || status === 403;
19
+ const missingWorkspace = status === 400 && errorText.includes("Workspace");
20
+ const guidance = isAuthError
21
+ ? "Sellable authentication failed.\n\n" +
22
+ `Update ${getConfigPath()} with a valid token from Sellable Settings -> Integrations, then retry.\n\n` +
23
+ "NOTE: If the token was just updated via the LLM (editing the config file), " +
24
+ "the change should take effect immediately. If it still fails, restart Claude Code to restart the MCP server."
25
+ : missingWorkspace
26
+ ? "No active workspace selected.\n\n" +
27
+ "Run list_workspaces then set_active_workspace to choose a workspace."
28
+ : undefined;
29
+ return new SellableApiError(status, errorText, guidance);
30
+ }
31
+ async requestResponse(method, path, body) {
18
32
  // Re-read config on every request so workspace switches take effect immediately
19
33
  const config = getConfig();
20
34
  const url = `${config.apiUrl}${path}`;
@@ -30,24 +44,21 @@ export class SellableApi {
30
44
  });
31
45
  if (!response.ok) {
32
46
  const errorText = await response.text();
33
- const isAuthError = response.status === 401 || response.status === 403;
34
- const missingWorkspace = response.status === 400 && errorText.includes("Workspace");
35
- const guidance = isAuthError
36
- ? "Sellable authentication failed.\n\n" +
37
- `Update ${getConfigPath()} with a valid token from Sellable Settings -> Integrations, then retry.\n\n` +
38
- "NOTE: If the token was just updated via the LLM (editing the config file), " +
39
- "the change should take effect immediately. If it still fails, restart Claude Code to restart the MCP server."
40
- : missingWorkspace
41
- ? "No active workspace selected.\n\n" +
42
- "Run list_workspaces then set_active_workspace to choose a workspace."
43
- : undefined;
44
- throw new SellableApiError(response.status, errorText, guidance);
47
+ throw this.buildError(response.status, errorText);
45
48
  }
49
+ return response;
50
+ }
51
+ async request(method, path, body) {
52
+ const response = await this.requestResponse(method, path, body);
46
53
  return response.json();
47
54
  }
48
55
  async get(path) {
49
56
  return this.request("GET", path);
50
57
  }
58
+ async getText(path) {
59
+ const response = await this.requestResponse("GET", path);
60
+ return response.text();
61
+ }
51
62
  async post(path, body) {
52
63
  return this.request("POST", path, body);
53
64
  }
package/dist/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -5,12 +5,12 @@ import { getSkillByName, listSkills } from "./skills.js";
5
5
  import { getAuthStatus } from "./tools/auth.js";
6
6
  import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
8
- import { createCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
9
8
  import { getCampaignTableSchema, queueCampaignCells, reviseMessageTemplateAndRerun, selectCampaignCells, waitForCampaignProcessing, } from "./tools/campaign-processing.js";
9
+ import { createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
10
10
  import { queueCells, updateCell } from "./tools/cells.js";
11
11
  import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
12
+ import { capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, getPublishedPostTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
12
13
  import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
13
- import { capturePostIdeaTool, getPublishedPostTool, getPostDraftTool, getPostIdeaTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
14
14
  import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
15
15
  import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
16
16
  import { searchEngagementPosts } from "./tools/engage-discovery.js";
@@ -18,7 +18,7 @@ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, reco
18
18
  import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
19
19
  import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
20
20
  import { getCampaignFramework } from "./tools/framework.js";
21
- import { cancelLeadImport, confirmProspeoCompanyAccounts, confirmLeadList, getProviderPrompt, importLeads, listDncEntriesTool, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeoCompanies, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
21
+ import { cancelLeadImport, confirmLeadList, confirmProspeoCompanyAccounts, getProviderPrompt, importLeads, loadCsvDncEntriesTool, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchProspeoCompanies, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
22
22
  import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
23
23
  import { getCampaignNavigationState } from "./tools/navigation.js";
24
24
  import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
@@ -30,7 +30,7 @@ import { getRows, getTableRows, getTableRowsMinimal } from "./tools/rows.js";
30
30
  import { addRubricItem, checkRubric, deleteRubricItem, draftRubrics, saveRubrics, selectNecessaryRubrics, updateRubricItem, waitForRubricResults, } from "./tools/rubrics.js";
31
31
  import { getSender, listSenders } from "./tools/senders.js";
32
32
  import { attachRecommendedSequence, attachSequence, createWorkflowTable, } from "./tools/sequencer.js";
33
- import { listTables } from "./tools/tables.js";
33
+ import { exportTableCsv, listTables } from "./tools/tables.js";
34
34
  import { handleVerifyTableRow } from "./tools/verify-row.js";
35
35
  import { sanitizeWatchUrlsForMcpResult } from "./tools/watch-url-security.js";
36
36
  import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, } from "./tools/workspaces.js";
@@ -208,6 +208,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
208
208
  case "pause_campaign":
209
209
  result = await pauseCampaign(args?.campaignId);
210
210
  break;
211
+ case "duplicate_campaign":
212
+ result = await duplicateCampaign(args?.campaignId);
213
+ if (result?.campaignOfferId) {
214
+ markCampaignContextDirty(result.campaignOfferId, "duplicate_campaign");
215
+ }
216
+ break;
211
217
  case "update_campaign_brief":
212
218
  result = await updateCampaignBrief(args?.campaignId, args?.campaignBrief);
213
219
  if (args?.campaignId) {
@@ -277,6 +283,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
277
283
  case "list_tables":
278
284
  result = await listTables(args);
279
285
  break;
286
+ case "export_table_csv":
287
+ result = await exportTableCsv(args);
288
+ break;
280
289
  case "commit_blueprint":
281
290
  result = await handleCommitBlueprint(args);
282
291
  break;
@@ -366,9 +375,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
366
375
  case "load_csv_dnc_entries":
367
376
  result = await loadCsvDncEntriesTool(args);
368
377
  break;
369
- case "list_dnc_entries":
370
- result = await listDncEntriesTool(args);
371
- break;
372
378
  case "save_domain_filters":
373
379
  result = await saveDomainFilters(args);
374
380
  break;
@@ -150,6 +150,42 @@ export interface UpdateCampaignInput {
150
150
  rubric?: unknown[];
151
151
  }
152
152
  export declare const campaignToolDefinitions: ({
153
+ name: string;
154
+ description: string;
155
+ inputSchema: {
156
+ type: string;
157
+ properties: {
158
+ campaignId: {
159
+ type: string;
160
+ description: string;
161
+ };
162
+ limit?: undefined;
163
+ tableId?: undefined;
164
+ leadLimit?: undefined;
165
+ page?: undefined;
166
+ filters?: undefined;
167
+ name?: undefined;
168
+ clientProspectId?: undefined;
169
+ senderLinkedinUrl?: undefined;
170
+ offerPositioning?: undefined;
171
+ campaignBrief?: undefined;
172
+ messageGenerationMode?: undefined;
173
+ currentStep?: undefined;
174
+ watchNarration?: undefined;
175
+ leadSourceType?: undefined;
176
+ leadSourceProvider?: undefined;
177
+ selectedLeadListId?: undefined;
178
+ senderIds?: undefined;
179
+ interactionMode?: undefined;
180
+ enableICPFilters?: undefined;
181
+ useMessagingTemplate?: undefined;
182
+ rubric?: undefined;
183
+ flowVersion?: undefined;
184
+ };
185
+ required: string[];
186
+ additionalProperties: boolean;
187
+ };
188
+ } | {
153
189
  name: string;
154
190
  description: string;
155
191
  inputSchema: {
@@ -600,6 +636,11 @@ export declare function startCampaign(campaignId: string): Promise<{
600
636
  export declare function pauseCampaign(campaignId: string): Promise<{
601
637
  success: boolean;
602
638
  }>;
639
+ export declare function duplicateCampaign(campaignId: string): Promise<{
640
+ campaignOfferId: string;
641
+ campaignName: string;
642
+ workflowTableId: string | null;
643
+ }>;
603
644
  export declare function updateCampaignBrief(campaignId: string, campaignBrief: string): Promise<{
604
645
  success: boolean;
605
646
  campaignBrief: string;
@@ -49,8 +49,7 @@ function getCampaignBuilderWatchModeFromUrl(watchUrl) {
49
49
  export function getCampaignBuilderWatchModeDriverLabel(mode = getCampaignBuilderWatchModeParam()) {
50
50
  return mode === "codex" ? "Codex" : "Claude Code";
51
51
  }
52
- export function buildCampaignWatchHandoffMarkdown(watchUrl, mode = getCampaignBuilderWatchModeFromUrl(watchUrl) ??
53
- getCampaignBuilderWatchModeParam()) {
52
+ export function buildCampaignWatchHandoffMarkdown(watchUrl, mode = getCampaignBuilderWatchModeFromUrl(watchUrl) ?? getCampaignBuilderWatchModeParam()) {
54
53
  const driverLabel = getCampaignBuilderWatchModeDriverLabel(mode);
55
54
  const headline = `WATCH ${driverLabel.toUpperCase()} BUILD THE CAMPAIGN LIVE`;
56
55
  return [
@@ -85,6 +84,21 @@ function assertBriefHandoffWatchUrl(watchUrl, campaignId) {
85
84
  "with create_campaign({ campaignId }) or get_campaign before asking for approval.");
86
85
  }
87
86
  export const campaignToolDefinitions = [
87
+ {
88
+ name: "duplicate_campaign",
89
+ description: "Duplicate a campaign by calling Sellable's existing duplicate endpoint. Returns campaignOfferId, campaignName, and workflowTableId for the new copy.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ campaignId: {
94
+ type: "string",
95
+ description: "Source campaign ID to duplicate.",
96
+ },
97
+ },
98
+ required: ["campaignId"],
99
+ additionalProperties: false,
100
+ },
101
+ },
88
102
  {
89
103
  name: "get_campaigns",
90
104
  description: "List campaigns for the authenticated user. Returns id, name, createdAt. Ordered by most recent first.",
@@ -845,7 +859,8 @@ export async function createCampaign(input) {
845
859
  name,
846
860
  clientProspectId,
847
861
  offerPositioning,
848
- ...(shouldPersistSourceStateOnCreate && input.leadSourceProvider !== undefined
862
+ ...(shouldPersistSourceStateOnCreate &&
863
+ input.leadSourceProvider !== undefined
849
864
  ? { leadSourceProvider: normalizedLeadSourceProvider }
850
865
  : {}),
851
866
  currentStep: effectiveCurrentStep,
@@ -950,6 +965,10 @@ export async function pauseCampaign(campaignId) {
950
965
  const api = getApi();
951
966
  return api.post(`/api/v3/campaigns/${campaignId}/pause`);
952
967
  }
968
+ export async function duplicateCampaign(campaignId) {
969
+ const api = getApi();
970
+ return api.post(`/api/v3/campaigns/${campaignId}/duplicate`);
971
+ }
953
972
  export async function updateCampaignBrief(campaignId, campaignBrief) {
954
973
  const api = getApi();
955
974
  return api.patch(`/api/v3/mcp/campaigns/${campaignId}`, { campaignBrief });
@@ -1,4 +1,3 @@
1
- type DncSourceType = "all" | "manual" | "hubspot";
2
1
  type InvalidDncRow = {
3
2
  row: number;
4
3
  column: string | null;
@@ -15,41 +14,6 @@ export type LoadCsvDncEntriesInput = {
15
14
  confirmationToken?: string;
16
15
  confirmed?: boolean;
17
16
  };
18
- export type ListDncEntriesInput = {
19
- page?: number;
20
- limit?: number;
21
- search?: string;
22
- listName?: string;
23
- sourceType?: DncSourceType;
24
- includeDeleted?: boolean;
25
- };
26
- export declare function listDncEntries(input?: ListDncEntriesInput): Promise<{
27
- ok: boolean;
28
- workspaceId: string;
29
- workspaceName: string;
30
- pagination: {
31
- total: number;
32
- page: number;
33
- limit: number;
34
- totalPages: number;
35
- };
36
- listNames: string[];
37
- entries: {
38
- id: string | null;
39
- type: string;
40
- domain: string | null;
41
- linkedin: string | null;
42
- linkedinUsername: string | null;
43
- name: string | null;
44
- source: {
45
- type: string;
46
- label: string;
47
- listName: string | null;
48
- };
49
- createdAt: string | null;
50
- }[];
51
- guidance: string;
52
- }>;
53
17
  export declare function loadCsvDncEntries(input: LoadCsvDncEntriesInput): Promise<{
54
18
  guidance: string;
55
19
  createdCount: number;
@@ -14,7 +14,6 @@ const CONFIRMATION_TOKEN_VERSION = "csv-dnc-preview-v1";
14
14
  const confirmationSecret = randomBytes(32).toString("hex");
15
15
  const MAX_DNC_UPLOAD_BYTES = 5 * 1024 * 1024;
16
16
  const MAX_DNC_CANDIDATES = 7500;
17
- const MAX_DNC_LIST_LIMIT = 100;
18
17
  const STRONG_DOMAIN_HEADER_KEYS = new Set([
19
18
  "domain",
20
19
  "website",
@@ -326,84 +325,16 @@ async function getVerifiedActiveWorkspace() {
326
325
  const config = getConfig();
327
326
  const activeWorkspaceId = config.activeWorkspaceId || config.workspaceId;
328
327
  if (!activeWorkspaceId) {
329
- throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.");
328
+ throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace before importing DNC entries.");
330
329
  }
331
330
  const api = getApi();
332
331
  const { workspaces } = await api.get("/api/v3/workspaces");
333
332
  const match = workspaces.find((workspace) => workspace.id === activeWorkspaceId);
334
333
  if (!match) {
335
- throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the exact workspace whose Sellable DNC list should be read or updated.`);
334
+ throw new Error(`Active workspace ${activeWorkspaceId} is not in the server access list. Run list_workspaces then set_active_workspace for the workspace that should receive the DNC import.`);
336
335
  }
337
336
  return match;
338
337
  }
339
- function clampListPositiveInt(value, fallback, max) {
340
- const parsed = typeof value === "number"
341
- ? value
342
- : Number.parseInt(String(value ?? ""), 10);
343
- if (!Number.isFinite(parsed) || parsed < 1)
344
- return fallback;
345
- return Math.min(Math.floor(parsed), max);
346
- }
347
- function normalizeDncSourceType(value) {
348
- if (value === undefined || value === null || value === "")
349
- return "all";
350
- if (value === "all" || value === "manual" || value === "hubspot") {
351
- return value;
352
- }
353
- throw new Error("sourceType must be one of all, manual, or hubspot.");
354
- }
355
- function optionalTrimmed(value) {
356
- return typeof value === "string" && value.trim() ? value.trim() : null;
357
- }
358
- function normalizeCreatedAt(value) {
359
- if (value instanceof Date)
360
- return value.toISOString();
361
- return optionalTrimmed(value);
362
- }
363
- function compactDncListEntry(entry) {
364
- const domain = optionalTrimmed(entry.domain);
365
- const linkedin = optionalTrimmed(entry.normalizedLinkedinUrl);
366
- const sourceType = optionalTrimmed(entry.sourceType) ?? "manual";
367
- return {
368
- id: optionalTrimmed(entry.id),
369
- type: domain ? "domain" : "linkedin",
370
- domain,
371
- linkedin,
372
- linkedinUsername: optionalTrimmed(entry.linkedinUsername),
373
- name: optionalTrimmed(entry.name),
374
- source: {
375
- type: sourceType,
376
- label: optionalTrimmed(entry.sourceLabel) ??
377
- (sourceType === "hubspot" ? "HubSpot" : "Manual"),
378
- listName: optionalTrimmed(entry.sourceListName),
379
- },
380
- createdAt: normalizeCreatedAt(entry.createdAt),
381
- };
382
- }
383
- function buildDncListPath(input) {
384
- const page = clampListPositiveInt(input.page, 1, Number.MAX_SAFE_INTEGER);
385
- const limit = clampListPositiveInt(input.limit, 25, MAX_DNC_LIST_LIMIT);
386
- const sourceType = normalizeDncSourceType(input.sourceType);
387
- const params = new URLSearchParams({
388
- page: String(page),
389
- limit: String(limit),
390
- });
391
- const search = optionalTrimmed(input.search);
392
- const listName = optionalTrimmed(input.listName);
393
- if (search)
394
- params.set("search", search);
395
- if (listName)
396
- params.set("listName", listName);
397
- if (sourceType !== "all")
398
- params.set("sourceType", sourceType);
399
- if (input.includeDeleted === true)
400
- params.set("includeDeleted", "true");
401
- return {
402
- path: `/api/v3/dnc-list?${params.toString()}`,
403
- page,
404
- limit,
405
- };
406
- }
407
338
  function makeSignature(payload) {
408
339
  return createHmac("sha256", confirmationSecret)
409
340
  .update(JSON.stringify(payload))
@@ -625,29 +556,6 @@ async function runDncSpotChecks(entries) {
625
556
  }
626
557
  return checks;
627
558
  }
628
- export async function listDncEntries(input = {}) {
629
- const workspace = await getVerifiedActiveWorkspace();
630
- const api = getApi();
631
- const request = buildDncListPath(input);
632
- const response = await api.get(request.path);
633
- const pagination = response.pagination ?? {};
634
- return {
635
- ok: true,
636
- workspaceId: workspace.id,
637
- workspaceName: workspace.name,
638
- pagination: {
639
- total: pagination.total ?? 0,
640
- page: pagination.page ?? request.page,
641
- limit: pagination.limit ?? request.limit,
642
- totalPages: pagination.totalPages ?? 0,
643
- },
644
- listNames: Array.isArray(response.listNames) ? response.listNames : [],
645
- entries: Array.isArray(response.entries)
646
- ? response.entries.map(compactDncListEntry)
647
- : [],
648
- guidance: "This is Sellable's workspace DNC list used by DNC Check.",
649
- };
650
- }
651
559
  export async function loadCsvDncEntries(input) {
652
560
  const workspace = await getVerifiedActiveWorkspace();
653
561
  const api = getApi();