@sellable/mcp 0.1.880 → 0.1.882

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.
@@ -45,6 +45,7 @@ export declare const inboxToolDefinitions: ({
45
45
  type: string;
46
46
  };
47
47
  threadId?: undefined;
48
+ messageLimit?: undefined;
48
49
  versionId?: undefined;
49
50
  body?: undefined;
50
51
  approvedBody?: undefined;
@@ -69,6 +70,54 @@ export declare const inboxToolDefinitions: ({
69
70
  openWorldHint: boolean;
70
71
  idempotentHint?: undefined;
71
72
  };
73
+ } | {
74
+ name: string;
75
+ description: string;
76
+ inputSchema: {
77
+ type: string;
78
+ properties: {
79
+ threadId: {
80
+ type: string;
81
+ };
82
+ messageLimit: {
83
+ type: string;
84
+ minimum: number;
85
+ maximum: number;
86
+ description: string;
87
+ };
88
+ limit?: undefined;
89
+ cursor?: undefined;
90
+ filter?: undefined;
91
+ status?: undefined;
92
+ senderId?: undefined;
93
+ classification?: undefined;
94
+ search?: undefined;
95
+ includeEnrichment?: undefined;
96
+ includeReplyEligibility?: undefined;
97
+ versionId?: undefined;
98
+ body?: undefined;
99
+ approvedBody?: undefined;
100
+ approvedBodyHash?: undefined;
101
+ approvedThreadSnapshot?: undefined;
102
+ idempotencyKey?: undefined;
103
+ approval?: undefined;
104
+ workspaceId?: undefined;
105
+ message?: undefined;
106
+ approvedMessageHash?: undefined;
107
+ };
108
+ required: string[];
109
+ additionalProperties: boolean;
110
+ };
111
+ outputSchema: {
112
+ type: string;
113
+ additionalProperties: boolean;
114
+ };
115
+ annotations: {
116
+ readOnlyHint: boolean;
117
+ destructiveHint: boolean;
118
+ openWorldHint: boolean;
119
+ idempotentHint?: undefined;
120
+ };
72
121
  } | {
73
122
  name: string;
74
123
  description: string;
@@ -87,6 +136,7 @@ export declare const inboxToolDefinitions: ({
87
136
  search?: undefined;
88
137
  includeEnrichment?: undefined;
89
138
  includeReplyEligibility?: undefined;
139
+ messageLimit?: undefined;
90
140
  versionId?: undefined;
91
141
  body?: undefined;
92
142
  approvedBody?: undefined;
@@ -135,6 +185,7 @@ export declare const inboxToolDefinitions: ({
135
185
  search?: undefined;
136
186
  includeEnrichment?: undefined;
137
187
  includeReplyEligibility?: undefined;
188
+ messageLimit?: undefined;
138
189
  approvedBody?: undefined;
139
190
  approvedBodyHash?: undefined;
140
191
  approvedThreadSnapshot?: undefined;
@@ -216,6 +267,7 @@ export declare const inboxToolDefinitions: ({
216
267
  search?: undefined;
217
268
  includeEnrichment?: undefined;
218
269
  includeReplyEligibility?: undefined;
270
+ messageLimit?: undefined;
219
271
  body?: undefined;
220
272
  message?: undefined;
221
273
  approvedMessageHash?: undefined;
@@ -289,6 +341,7 @@ export declare const inboxToolDefinitions: ({
289
341
  search?: undefined;
290
342
  includeEnrichment?: undefined;
291
343
  includeReplyEligibility?: undefined;
344
+ messageLimit?: undefined;
292
345
  versionId?: undefined;
293
346
  body?: undefined;
294
347
  approvedBody?: undefined;
@@ -309,7 +362,51 @@ export declare const inboxToolDefinitions: ({
309
362
  };
310
363
  })[];
311
364
  export declare function searchInboxThreads(input: Record<string, unknown>): Promise<ToolResult<{
365
+ ok: boolean;
366
+ error: string;
367
+ message: string;
368
+ }> | ToolResult<{
312
369
  routeCalled: string;
370
+ threads: {
371
+ id: string | null;
372
+ leadName: string | null;
373
+ leadProfileUrl: string | null;
374
+ senderName: string | null;
375
+ senderProviderId: string | null;
376
+ chatType: string | null;
377
+ classification: string | null;
378
+ latestMessage: {
379
+ id: string | null;
380
+ direction: string | null;
381
+ timestamp: string | null;
382
+ preview: string | null;
383
+ } | null;
384
+ draft: {
385
+ id: string | null;
386
+ versionId: string | null;
387
+ status: string | null;
388
+ versionNumber: number | null;
389
+ createdAt: string | null;
390
+ } | null;
391
+ enrichment: Record<string, unknown> | null;
392
+ campaign: Record<string, unknown> | null;
393
+ latestMessageTimestamp: string | null;
394
+ updatedAt: string | null;
395
+ hasUnprocessedInbound: boolean | null;
396
+ lastInboundMessageAt: string | null;
397
+ followUpsSuppressed: boolean | null;
398
+ replyEligibility: {
399
+ canSendDraft: boolean | null;
400
+ canSendManual: boolean | null;
401
+ reasonCodes: (string | null)[];
402
+ } | null;
403
+ }[];
404
+ pagination: {} | null;
405
+ totalCount: {};
406
+ filters: {} | null;
407
+ status: {} | null;
408
+ filter: {} | null;
409
+ resultShape: string;
313
410
  }>>;
314
411
  export declare function getInboxThread(input: Record<string, unknown>): Promise<ToolResult<any>>;
315
412
  export declare function checkInboxReplyEligibility(input: Record<string, unknown>): Promise<ToolResult<any>>;
@@ -1,5 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { getApi, SellableApiError } from "../api.js";
3
+ const MAX_INBOX_TOOL_RESULT_BYTES = 128 * 1024;
4
+ const MAX_INBOX_SEARCH_LIMIT = 25;
5
+ const MAX_INBOX_DETAIL_MESSAGES = 50;
3
6
  const snapshotSchema = {
4
7
  type: "object",
5
8
  properties: {
@@ -17,11 +20,15 @@ const resultOutputSchema = {
17
20
  export const inboxToolDefinitions = [
18
21
  {
19
22
  name: "search_inbox_threads",
20
- description: "Search LinkedIn inbox threads using the existing Sellable product inbox route. This calls GET /api/v3/inbox and returns thread summaries for the active workspace.",
23
+ description: "Search LinkedIn inbox threads using the existing Sellable product inbox route. Returns bounded compact summaries for the active workspace; use get_inbox_thread only for a relevant thread.",
21
24
  inputSchema: {
22
25
  type: "object",
23
26
  properties: {
24
- limit: { type: "number", minimum: 1, maximum: 50 },
27
+ limit: {
28
+ type: "number",
29
+ minimum: 1,
30
+ maximum: MAX_INBOX_SEARCH_LIMIT,
31
+ },
25
32
  cursor: { type: "string" },
26
33
  filter: { type: "string", enum: ["all", "drafts"] },
27
34
  status: {
@@ -46,11 +53,17 @@ export const inboxToolDefinitions = [
46
53
  },
47
54
  {
48
55
  name: "get_inbox_thread",
49
- description: "Fetch one LinkedIn inbox thread using the existing Sellable product route GET /api/v3/inbox/[threadId]. Returns the product payload plus a chronological transcript for MCP review.",
56
+ description: "Fetch one LinkedIn inbox thread using the existing Sellable product route GET /api/v3/inbox/[threadId]. Returns bounded thread detail plus a recent chronological transcript for MCP review.",
50
57
  inputSchema: {
51
58
  type: "object",
52
59
  properties: {
53
60
  threadId: { type: "string" },
61
+ messageLimit: {
62
+ type: "number",
63
+ minimum: 1,
64
+ maximum: MAX_INBOX_DETAIL_MESSAGES,
65
+ description: "Maximum recent messages to return in chronological order (default 25, max 50).",
66
+ },
54
67
  },
55
68
  required: ["threadId"],
56
69
  additionalProperties: false,
@@ -187,6 +200,145 @@ function textResult(structuredContent, text, isError = false) {
187
200
  function errorResult(error, message, extra = {}) {
188
201
  return textResult({ ok: false, error, message, ...extra }, message, true);
189
202
  }
203
+ function utf8Bytes(value) {
204
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
205
+ }
206
+ function boundedString(value, maxChars) {
207
+ if (typeof value !== "string")
208
+ return null;
209
+ return value.length <= maxChars
210
+ ? value
211
+ : `${value.slice(0, Math.max(0, maxChars - 1))}…`;
212
+ }
213
+ function asRecord(value) {
214
+ return value && typeof value === "object" && !Array.isArray(value)
215
+ ? value
216
+ : null;
217
+ }
218
+ function compactRecord(value, fields) {
219
+ const source = asRecord(value);
220
+ if (!source)
221
+ return null;
222
+ const result = {};
223
+ for (const [field, maxChars] of fields) {
224
+ const fieldValue = source[field];
225
+ if (typeof fieldValue === "string") {
226
+ result[field] = boundedString(fieldValue, maxChars);
227
+ }
228
+ else if (typeof fieldValue === "number" ||
229
+ typeof fieldValue === "boolean" ||
230
+ fieldValue === null) {
231
+ result[field] = fieldValue;
232
+ }
233
+ }
234
+ return Object.keys(result).length > 0 ? result : null;
235
+ }
236
+ function compactThreadSummary(value) {
237
+ const thread = asRecord(value) ?? {};
238
+ const messages = Array.isArray(thread.messages) ? thread.messages : [];
239
+ const latestMessage = asRecord(messages[0]);
240
+ const draft = asRecord(thread.draft);
241
+ const replyEligibility = asRecord(thread.replyEligibility);
242
+ return {
243
+ id: boundedString(thread.id, 256),
244
+ leadName: boundedString(thread.leadName, 256),
245
+ leadProfileUrl: boundedString(thread.leadProfileUrl, 1_024),
246
+ senderName: boundedString(thread.senderName, 256),
247
+ senderProviderId: boundedString(thread.senderProviderId, 256),
248
+ chatType: boundedString(thread.chatType, 32),
249
+ classification: boundedString(thread.classification, 128),
250
+ latestMessage: latestMessage
251
+ ? {
252
+ id: boundedString(latestMessage.id, 256),
253
+ direction: boundedString(latestMessage.direction, 32),
254
+ timestamp: normalizeTimestamp(latestMessage.timestamp),
255
+ preview: boundedString(latestMessage.message ?? latestMessage.body, 500),
256
+ }
257
+ : null,
258
+ draft: draft
259
+ ? {
260
+ id: boundedString(draft.id, 256),
261
+ versionId: boundedString(draft.versionId, 256),
262
+ status: boundedString(draft.status, 64),
263
+ versionNumber: typeof draft.versionNumber === "number"
264
+ ? draft.versionNumber
265
+ : null,
266
+ createdAt: normalizeTimestamp(draft.createdAt),
267
+ }
268
+ : null,
269
+ enrichment: compactRecord(thread.enrichment, [
270
+ ["name", 256],
271
+ ["title", 256],
272
+ ["company", 256],
273
+ ["companyWebsiteUrl", 1_024],
274
+ ["location", 256],
275
+ ["linkedinUrl", 1_024],
276
+ ]),
277
+ campaign: compactRecord(thread.campaign, [
278
+ ["id", 256],
279
+ ["name", 256],
280
+ ]),
281
+ latestMessageTimestamp: normalizeTimestamp(thread.latestMessageTimestamp),
282
+ updatedAt: normalizeTimestamp(thread.updatedAt),
283
+ hasUnprocessedInbound: typeof thread.hasUnprocessedInbound === "boolean"
284
+ ? thread.hasUnprocessedInbound
285
+ : null,
286
+ lastInboundMessageAt: normalizeTimestamp(thread.lastInboundMessageAt),
287
+ followUpsSuppressed: typeof thread.followUpsSuppressed === "boolean"
288
+ ? thread.followUpsSuppressed
289
+ : null,
290
+ replyEligibility: replyEligibility
291
+ ? {
292
+ canSendDraft: typeof replyEligibility.canSendDraft === "boolean"
293
+ ? replyEligibility.canSendDraft
294
+ : null,
295
+ canSendManual: typeof replyEligibility.canSendManual === "boolean"
296
+ ? replyEligibility.canSendManual
297
+ : null,
298
+ reasonCodes: Array.isArray(replyEligibility.reasonCodes)
299
+ ? replyEligibility.reasonCodes
300
+ .slice(0, 20)
301
+ .map((reason) => boundedString(reason, 128))
302
+ : [],
303
+ }
304
+ : null,
305
+ };
306
+ }
307
+ function compactThreadDetail(thread) {
308
+ if (!thread)
309
+ return null;
310
+ const source = thread;
311
+ const draft = asRecord(source.draft);
312
+ return {
313
+ ...compactThreadSummary(source),
314
+ messageOrder: boundedString(source.messageOrder, 32),
315
+ draft: draft
316
+ ? {
317
+ id: boundedString(draft.id, 256),
318
+ versionId: boundedString(draft.versionId, 256),
319
+ draftId: boundedString(draft.draftId, 256),
320
+ body: boundedString(draft.body, 16_000),
321
+ subject: boundedString(draft.subject, 2_000),
322
+ status: boundedString(draft.status, 64),
323
+ versionNumber: typeof draft.versionNumber === "number"
324
+ ? draft.versionNumber
325
+ : null,
326
+ createdAt: normalizeTimestamp(draft.createdAt),
327
+ }
328
+ : null,
329
+ };
330
+ }
331
+ function compactTranscriptMessage(value) {
332
+ const message = asRecord(value) ?? {};
333
+ return {
334
+ id: boundedString(message.id, 256),
335
+ message: boundedString(message.message ?? message.body, 8_000),
336
+ subject: boundedString(message.subject, 2_000),
337
+ timestamp: normalizeTimestamp(message.timestamp),
338
+ direction: boundedString(message.direction, 32),
339
+ senderProviderId: boundedString(message.senderProviderId, 256),
340
+ };
341
+ }
190
342
  function requireString(value, field) {
191
343
  if (typeof value !== "string" || value.trim() === "") {
192
344
  return errorResult("invalid_arguments", `${field} is required.`);
@@ -253,6 +405,39 @@ function chronologicalTranscript(thread) {
253
405
  return aTime - bTime;
254
406
  });
255
407
  }
408
+ function boundedTranscriptResult(input) {
409
+ const chronological = chronologicalTranscript(input.thread);
410
+ const selected = chronological
411
+ .slice(-input.messageLimit)
412
+ .map(compactTranscriptMessage);
413
+ const compactThread = compactThreadDetail(input.thread);
414
+ const acceptedChronological = [];
415
+ for (const message of [...selected].reverse()) {
416
+ const candidate = [message, ...acceptedChronological];
417
+ const payload = {
418
+ routeCalled: input.routeCalled,
419
+ thread: compactThread,
420
+ messageOrder: "chronological",
421
+ chronologicalTranscript: candidate,
422
+ messagesAvailable: chronological.length,
423
+ messagesReturned: candidate.length,
424
+ transcriptTruncated: candidate.length < chronological.length,
425
+ };
426
+ if (utf8Bytes(payload) > MAX_INBOX_TOOL_RESULT_BYTES)
427
+ break;
428
+ acceptedChronological.unshift(message);
429
+ }
430
+ const transcript = acceptedChronological;
431
+ return {
432
+ routeCalled: input.routeCalled,
433
+ thread: compactThread,
434
+ messageOrder: "chronological",
435
+ chronologicalTranscript: transcript,
436
+ messagesAvailable: chronological.length,
437
+ messagesReturned: transcript.length,
438
+ transcriptTruncated: transcript.length < chronological.length,
439
+ };
440
+ }
256
441
  function routeError(error, fallback) {
257
442
  if (error instanceof SellableApiError) {
258
443
  let parsed = null;
@@ -278,7 +463,7 @@ function appendParam(params, key, value) {
278
463
  export async function searchInboxThreads(input) {
279
464
  const params = new URLSearchParams();
280
465
  const limit = typeof input.limit === "number" && Number.isFinite(input.limit)
281
- ? Math.max(1, Math.min(Math.trunc(input.limit), 50))
466
+ ? Math.max(1, Math.min(Math.trunc(input.limit), MAX_INBOX_SEARCH_LIMIT))
282
467
  : undefined;
283
468
  appendParam(params, "limit", limit);
284
469
  appendParam(params, "cursor", input.cursor);
@@ -296,11 +481,23 @@ export async function searchInboxThreads(input) {
296
481
  const query = params.toString();
297
482
  const routeCalled = `/api/v3/inbox${query ? `?${query}` : ""}`;
298
483
  const response = await getApi().get(routeCalled);
299
- const threads = Array.isArray(response.threads) ? response.threads : [];
300
- return textResult({
484
+ const threads = Array.isArray(response.threads)
485
+ ? response.threads.map(compactThreadSummary)
486
+ : [];
487
+ const structuredContent = {
301
488
  routeCalled,
302
- ...response,
303
- }, `${threads.length} inbox thread${threads.length === 1 ? "" : "s"} returned.`);
489
+ threads,
490
+ pagination: response.pagination ?? null,
491
+ totalCount: response.totalCount ?? threads.length,
492
+ filters: response.filters ?? null,
493
+ status: response.status ?? null,
494
+ filter: response.filter ?? null,
495
+ resultShape: "compact_thread_summaries",
496
+ };
497
+ if (utf8Bytes(structuredContent) > MAX_INBOX_TOOL_RESULT_BYTES) {
498
+ return errorResult("inbox_search_result_too_large", "Inbox summaries exceeded the MCP response budget. Narrow the filters or page size.", { routeCalled, returnedCount: threads.length });
499
+ }
500
+ return textResult(structuredContent, `${threads.length} inbox thread${threads.length === 1 ? "" : "s"} returned.`);
304
501
  }
305
502
  export async function getInboxThread(input) {
306
503
  const required = requireStrings(input, ["threadId"]);
@@ -310,12 +507,16 @@ export async function getInboxThread(input) {
310
507
  const routeCalled = `/api/v3/inbox/${encodeURIComponent(threadId)}`;
311
508
  const response = await getApi().get(routeCalled);
312
509
  const thread = getThread(response);
313
- return textResult({
510
+ const messageLimit = typeof input.messageLimit === "number" &&
511
+ Number.isFinite(input.messageLimit)
512
+ ? Math.max(1, Math.min(Math.trunc(input.messageLimit), MAX_INBOX_DETAIL_MESSAGES))
513
+ : 25;
514
+ const structuredContent = boundedTranscriptResult({
314
515
  routeCalled,
315
516
  thread,
316
- messageOrder: thread?.messageOrder ?? "route_order",
317
- chronologicalTranscript: chronologicalTranscript(thread),
318
- }, thread
517
+ messageLimit,
518
+ });
519
+ return textResult(structuredContent, thread
319
520
  ? `Loaded inbox thread ${thread.id ?? threadId}.`
320
521
  : `Loaded inbox thread ${threadId}.`);
321
522
  }
@@ -259,7 +259,7 @@ export const integrationsToolDefinitions = [
259
259
  },
260
260
  {
261
261
  name: "integrations_call_tool",
262
- description: "Run one allowed provider action against a connected account. The exact account is resolved server-side from the binding — you never pass an account id, and one passed in the arguments is rejected. An action outside the reviewed allowlist is refused permanently: that is a policy decision, not a validation problem, so do not retry it with different arguments. An 'approval_required' outcome means a human must approve this exact call on the Integrations page first.",
262
+ description: "Run one allowed provider action against a connected account. The exact account is resolved server-side from the binding — you never pass an account id, and one passed in the arguments is rejected. An action outside the reviewed allowlist is refused permanently: that is a policy decision, not a validation problem, so do not retry it with different arguments. An 'approval_required' outcome is a chat-native pause: ask a verified human to approve or deny this exact call in the same chat. After a later approval message, retry once with the unchanged selector, toolName, and arguments plus the returned approvalId. No separate UI is required.",
263
263
  inputSchema: {
264
264
  type: "object",
265
265
  properties: {
@@ -279,7 +279,7 @@ export const integrationsToolDefinitions = [
279
279
  type: "string",
280
280
  minLength: 1,
281
281
  maxLength: 128,
282
- description: "The approval reference returned by a previous approval_required outcome for these EXACT arguments.",
282
+ description: "The approval reference returned by a previous approval_required outcome. Pass it only after a later verified human message in the same selected chat approves this EXACT call, and retry with the unchanged selector, toolName, and arguments.",
283
283
  },
284
284
  },
285
285
  required: ["toolName"],
@@ -9560,6 +9560,54 @@ export declare const allTools: ({
9560
9560
  required: string[];
9561
9561
  additionalProperties: boolean;
9562
9562
  };
9563
+ } | {
9564
+ name: string;
9565
+ description: string;
9566
+ inputSchema: {
9567
+ type: string;
9568
+ properties: {
9569
+ threadId: {
9570
+ type: string;
9571
+ };
9572
+ messageLimit: {
9573
+ type: string;
9574
+ minimum: number;
9575
+ maximum: number;
9576
+ description: string;
9577
+ };
9578
+ limit?: undefined;
9579
+ cursor?: undefined;
9580
+ filter?: undefined;
9581
+ status?: undefined;
9582
+ senderId?: undefined;
9583
+ classification?: undefined;
9584
+ search?: undefined;
9585
+ includeEnrichment?: undefined;
9586
+ includeReplyEligibility?: undefined;
9587
+ versionId?: undefined;
9588
+ body?: undefined;
9589
+ approvedBody?: undefined;
9590
+ approvedBodyHash?: undefined;
9591
+ approvedThreadSnapshot?: undefined;
9592
+ idempotencyKey?: undefined;
9593
+ approval?: undefined;
9594
+ workspaceId?: undefined;
9595
+ message?: undefined;
9596
+ approvedMessageHash?: undefined;
9597
+ };
9598
+ required: string[];
9599
+ additionalProperties: boolean;
9600
+ };
9601
+ outputSchema: {
9602
+ type: string;
9603
+ additionalProperties: boolean;
9604
+ };
9605
+ annotations: {
9606
+ readOnlyHint: boolean;
9607
+ destructiveHint: boolean;
9608
+ openWorldHint: boolean;
9609
+ idempotentHint?: undefined;
9610
+ };
9563
9611
  } | {
9564
9612
  name: string;
9565
9613
  description: string;
@@ -9578,6 +9626,7 @@ export declare const allTools: ({
9578
9626
  search?: undefined;
9579
9627
  includeEnrichment?: undefined;
9580
9628
  includeReplyEligibility?: undefined;
9629
+ messageLimit?: undefined;
9581
9630
  versionId?: undefined;
9582
9631
  body?: undefined;
9583
9632
  approvedBody?: undefined;
@@ -9626,6 +9675,7 @@ export declare const allTools: ({
9626
9675
  search?: undefined;
9627
9676
  includeEnrichment?: undefined;
9628
9677
  includeReplyEligibility?: undefined;
9678
+ messageLimit?: undefined;
9629
9679
  approvedBody?: undefined;
9630
9680
  approvedBodyHash?: undefined;
9631
9681
  approvedThreadSnapshot?: undefined;
@@ -9707,6 +9757,7 @@ export declare const allTools: ({
9707
9757
  search?: undefined;
9708
9758
  includeEnrichment?: undefined;
9709
9759
  includeReplyEligibility?: undefined;
9760
+ messageLimit?: undefined;
9710
9761
  body?: undefined;
9711
9762
  message?: undefined;
9712
9763
  approvedMessageHash?: undefined;
@@ -9780,6 +9831,7 @@ export declare const allTools: ({
9780
9831
  search?: undefined;
9781
9832
  includeEnrichment?: undefined;
9782
9833
  includeReplyEligibility?: undefined;
9834
+ messageLimit?: undefined;
9783
9835
  versionId?: undefined;
9784
9836
  body?: undefined;
9785
9837
  approvedBody?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.880",
3
+ "version": "0.1.882",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",