@floomhq/signaldash 0.13.0 → 0.27.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/README.md +249 -10
- package/bin/sd.mjs +1006 -38
- package/lib/cli.js +16 -22
- package/lib/config-file.js +114 -0
- package/lib/mcp.js +12 -2
- package/package.json +3 -3
- package/skills/signaldash/SKILL.md +1033 -62
- package/skills/signaldash-safe-usage/SKILL.md +74 -9
package/bin/sd.mjs
CHANGED
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
// tools an agent uses all proxy through the backend, so agents access channels
|
|
5
5
|
// THROUGH SignalDash, not around it.
|
|
6
6
|
import {
|
|
7
|
-
chmodSync,
|
|
8
7
|
readFileSync,
|
|
9
8
|
realpathSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
mkdirSync,
|
|
12
9
|
} from "node:fs";
|
|
13
10
|
import { homedir } from "node:os";
|
|
14
11
|
import { createInterface } from "node:readline";
|
|
15
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { readConfigFile, updateConfigFile } from "../lib/config-file.js";
|
|
16
14
|
|
|
17
15
|
// Lazy-load presentation deps so `mcp` (stdio, machine-facing) stays clean/fast.
|
|
18
16
|
async function ui() {
|
|
@@ -32,28 +30,41 @@ function configPaths() {
|
|
|
32
30
|
}
|
|
33
31
|
function loadCfg() {
|
|
34
32
|
try {
|
|
35
|
-
return
|
|
33
|
+
return readConfigFile(configPaths().file);
|
|
36
34
|
} catch {
|
|
37
35
|
return {};
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
mkdirSync(config.directory, { recursive: true, mode: 0o700 });
|
|
43
|
-
chmodSync(config.directory, 0o700);
|
|
44
|
-
writeFileSync(config.file, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
45
|
-
chmodSync(config.file, 0o600);
|
|
38
|
+
function updateCfg(update) {
|
|
39
|
+
return updateConfigFile(configPaths().file, update);
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
async function api(
|
|
42
|
+
async function api(
|
|
43
|
+
path,
|
|
44
|
+
body,
|
|
45
|
+
{ auth = true, method = "POST", backend, token } = {},
|
|
46
|
+
) {
|
|
49
47
|
const cfg = loadCfg();
|
|
50
|
-
const
|
|
51
|
-
const
|
|
48
|
+
const targetBackend = backend || cfg.backend || DEFAULT_BACKEND;
|
|
49
|
+
const targetToken = token || cfg.token;
|
|
50
|
+
const r = await fetch(targetBackend + path, {
|
|
52
51
|
method,
|
|
53
|
-
headers: {
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
...(auth && targetToken ? { Authorization: `Bearer ${targetToken}` } : {}),
|
|
55
|
+
},
|
|
54
56
|
...(method === "GET" ? {} : { body: JSON.stringify(body || {}) }),
|
|
55
57
|
});
|
|
56
|
-
|
|
58
|
+
const json = await r.json().catch(() => ({}));
|
|
59
|
+
if (
|
|
60
|
+
json &&
|
|
61
|
+
typeof json === "object" &&
|
|
62
|
+
typeof json.approval_path === "string" &&
|
|
63
|
+
json.approval_path.startsWith("/")
|
|
64
|
+
) {
|
|
65
|
+
json.approval_url = new URL(json.approval_path, targetBackend).toString();
|
|
66
|
+
}
|
|
67
|
+
return { status: r.status, json };
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
@@ -69,10 +80,12 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
69
80
|
const request = dependencies.request || api;
|
|
70
81
|
const log = dependencies.log || console.log;
|
|
71
82
|
const error = dependencies.error || console.error;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const targetBackend = backend || loadCfg().backend || DEFAULT_BACKEND;
|
|
84
|
+
const r = await request(
|
|
85
|
+
"/login",
|
|
86
|
+
{ code },
|
|
87
|
+
{ auth: false, backend: targetBackend },
|
|
88
|
+
);
|
|
76
89
|
if (r.status !== 200) {
|
|
77
90
|
const msg = String(r.json.error || r.status);
|
|
78
91
|
if (/used|invalid/i.test(msg) && loadCfg().token) {
|
|
@@ -83,8 +96,12 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
83
96
|
if (/used/i.test(msg)) error("Invite codes are single-use. If you already ran this, try: signaldash status");
|
|
84
97
|
process.exitCode = 1; return;
|
|
85
98
|
}
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
updateCfg(current => ({
|
|
100
|
+
...current,
|
|
101
|
+
backend: targetBackend,
|
|
102
|
+
token: r.json.token,
|
|
103
|
+
}));
|
|
104
|
+
log(`Logged in to SignalDash (${targetBackend}).`);
|
|
88
105
|
log("Your LinkedIn/WhatsApp/email credentials live on that server, not on this");
|
|
89
106
|
log("machine, and are never exposed to your agent. Only you can see your data.");
|
|
90
107
|
}
|
|
@@ -98,14 +115,24 @@ export async function cmdLogout(dependencies = {}) {
|
|
|
98
115
|
log("Already logged out.");
|
|
99
116
|
return;
|
|
100
117
|
}
|
|
101
|
-
const response = await request(
|
|
118
|
+
const response = await request(
|
|
119
|
+
"/logout",
|
|
120
|
+
{},
|
|
121
|
+
{ backend: cfg.backend, token: cfg.token },
|
|
122
|
+
);
|
|
102
123
|
if (response.status !== 200 && response.status !== 401) {
|
|
103
124
|
error("logout failed:", response.json.error || response.status);
|
|
104
125
|
process.exitCode = 1;
|
|
105
126
|
return;
|
|
106
127
|
}
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
updateCfg(current => {
|
|
129
|
+
if (current.backend !== cfg.backend || current.token !== cfg.token) {
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
const next = { ...current };
|
|
133
|
+
delete next.token;
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
109
136
|
log(
|
|
110
137
|
response.status === 200
|
|
111
138
|
? "Logged out of SignalDash. The session was revoked."
|
|
@@ -187,12 +214,822 @@ export async function cmdClaim(provider, accountId, dependencies = {}) {
|
|
|
187
214
|
|
|
188
215
|
// ---- MCP (stdio). Every tool proxies through the backend with the token. ----
|
|
189
216
|
const TOOLS = [
|
|
190
|
-
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
217
|
+
{
|
|
218
|
+
name: "li_list_chats",
|
|
219
|
+
ch: "li",
|
|
220
|
+
action: "list_chats",
|
|
221
|
+
description: "List or search your LinkedIn chats. `search` filters on identifiers, never on a person name (a 1:1 chat has no name); the response reports how far it scanned.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "How many chats to return. With `search`, how many MATCHES to return." },
|
|
226
|
+
cursor: { type: "string", maxLength: 4000, description: "Continue from a previous page or a previous search." },
|
|
227
|
+
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on name and counterpart identifiers. The provider cannot filter a chat list, so SignalDash filters over a bounded scan and the response reports `scanned_chats` and `exhaustive` so a miss is never mistaken for a proven absence. A 1:1 chat has no name, so search by identifier or phone number, not by person name." },
|
|
228
|
+
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "How many chats the search may scan before it stops and reports exhaustive:false. Default 200, and never more than five provider pages: bulk reading is the top account-restriction trigger, so go deeper with the returned cursor rather than with a bigger scan." },
|
|
229
|
+
},
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "li_read_messages",
|
|
235
|
+
ch: "li",
|
|
236
|
+
action: "read",
|
|
237
|
+
description: "Read messages in a LinkedIn chat. args: chat_id",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
242
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
243
|
+
},
|
|
244
|
+
required: ["chat_id"],
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "li_send_message",
|
|
250
|
+
ch: "li",
|
|
251
|
+
action: "send",
|
|
252
|
+
description: "Send a LinkedIn message (rate-safe). args: chat_id, text",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
257
|
+
text: { type: "string", minLength: 1, maxLength: 5000 },
|
|
258
|
+
},
|
|
259
|
+
required: ["chat_id", "text"],
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "li_send_invitation",
|
|
265
|
+
path: "/li/send_invitation",
|
|
266
|
+
description: "Preview or send one exact LinkedIn invitation. First call without confirm, obtain approval, then repeat the exact provider_id and note with confirm:true.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
provider_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
271
|
+
note: { type: "string", maxLength: 300 },
|
|
272
|
+
confirm: { type: "boolean", default: false },
|
|
273
|
+
},
|
|
274
|
+
required: ["provider_id"],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "li_invitations_received",
|
|
280
|
+
path: "/li/invitations/received",
|
|
281
|
+
description: "List a bounded page of received LinkedIn invitations and authorize exact later accept actions.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
286
|
+
cursor: { type: "string", maxLength: 1000 },
|
|
287
|
+
},
|
|
288
|
+
additionalProperties: false,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "li_accept_invitation",
|
|
293
|
+
path: "/li/accept_invitation",
|
|
294
|
+
description: "Accept one exact received LinkedIn invitation after listing current received invitations and obtaining approval.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
invitation_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
299
|
+
confirm: { type: "boolean" },
|
|
300
|
+
},
|
|
301
|
+
required: ["invitation_id", "confirm"],
|
|
302
|
+
additionalProperties: false,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "li_invitations_sent",
|
|
307
|
+
path: "/li/invitations/sent",
|
|
308
|
+
description: "List a bounded page of sent LinkedIn invitations and authorize exact later withdrawal actions.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
313
|
+
cursor: { type: "string", maxLength: 1000 },
|
|
314
|
+
},
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "li_withdraw_invitation",
|
|
320
|
+
path: "/li/withdraw_invitation",
|
|
321
|
+
description: "Withdraw one exact sent LinkedIn invitation after listing current sent invitations and obtaining approval.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
invitation_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
326
|
+
confirm: { type: "boolean" },
|
|
327
|
+
},
|
|
328
|
+
required: ["invitation_id", "confirm"],
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "sd_contact_state",
|
|
334
|
+
path: "/sd/contact_state",
|
|
335
|
+
description: "Inspect exact per-user, per-channel contact state or add a protective suppression. SignalDash never infers cross-channel identity.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
channel: {
|
|
340
|
+
type: "string",
|
|
341
|
+
enum: ["linkedin", "whatsapp", "email"],
|
|
342
|
+
},
|
|
343
|
+
identifiers: {
|
|
344
|
+
type: "array",
|
|
345
|
+
minItems: 1,
|
|
346
|
+
maxItems: 8,
|
|
347
|
+
items: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
kind: {
|
|
351
|
+
type: "string",
|
|
352
|
+
enum: [
|
|
353
|
+
"provider_id",
|
|
354
|
+
"public_identifier",
|
|
355
|
+
"profile_url",
|
|
356
|
+
"member_urn",
|
|
357
|
+
"chat_id",
|
|
358
|
+
"phone",
|
|
359
|
+
"email",
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
value: { type: "string", minLength: 1, maxLength: 1000 },
|
|
363
|
+
},
|
|
364
|
+
required: ["kind", "value"],
|
|
365
|
+
additionalProperties: false,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
action: {
|
|
369
|
+
type: "string",
|
|
370
|
+
enum: ["get", "suppress"],
|
|
371
|
+
default: "get",
|
|
372
|
+
},
|
|
373
|
+
reason: {
|
|
374
|
+
type: "string",
|
|
375
|
+
enum: [
|
|
376
|
+
"opt_out",
|
|
377
|
+
"not_interested",
|
|
378
|
+
"provider_block",
|
|
379
|
+
"manual",
|
|
380
|
+
"legal",
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
confirm: { type: "boolean", default: false },
|
|
384
|
+
},
|
|
385
|
+
required: ["channel", "identifiers"],
|
|
386
|
+
additionalProperties: false,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "sd_budget_status",
|
|
391
|
+
path: "/sd/budget_status",
|
|
392
|
+
description: "Read authoritative SignalDash LinkedIn attempt capacity for this UTC day and week across manual work and all campaigns. Reports the campaign-excluded portion without promising a reserved manual slot.",
|
|
393
|
+
inputSchema: {
|
|
394
|
+
type: "object",
|
|
395
|
+
properties: {},
|
|
396
|
+
additionalProperties: false,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "sd_settings_get",
|
|
401
|
+
path: "/sd/settings/get",
|
|
402
|
+
description: "Read this SignalDash user's persistent settings. Auto-accept is the first supported setting and is false by default for every existing user.",
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {},
|
|
406
|
+
additionalProperties: false,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "sd_settings_set",
|
|
411
|
+
path: "/sd/settings/set",
|
|
412
|
+
description: "Update this SignalDash user's persistent settings. Currently supports opt-in, rate-capped LinkedIn invitation auto-accept and exact optional filters. Changing a persistent setting requires explicit approval and confirm:true.",
|
|
413
|
+
inputSchema: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
auto_accept_linkedin: { type: "boolean" },
|
|
417
|
+
auto_accept_linkedin_filters: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
public_identifiers: {
|
|
421
|
+
type: "array",
|
|
422
|
+
maxItems: 100,
|
|
423
|
+
items: {
|
|
424
|
+
type: "string",
|
|
425
|
+
minLength: 1,
|
|
426
|
+
maxLength: 200,
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
description_keywords: {
|
|
430
|
+
type: "array",
|
|
431
|
+
maxItems: 20,
|
|
432
|
+
items: {
|
|
433
|
+
type: "string",
|
|
434
|
+
minLength: 1,
|
|
435
|
+
maxLength: 100,
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
additionalProperties: false,
|
|
440
|
+
},
|
|
441
|
+
confirm: { type: "boolean" },
|
|
442
|
+
},
|
|
443
|
+
required: ["auto_accept_linkedin", "confirm"],
|
|
444
|
+
additionalProperties: false,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "sd_auto_accept_status",
|
|
449
|
+
path: "/sd/auto_accept/status",
|
|
450
|
+
description: "Inspect this user's persistent auto-accept setting, filters, accepted/failed attempts today, remaining dedicated cap, repeated-error stop state, and any unparseable invitation records.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {},
|
|
454
|
+
additionalProperties: false,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: "sd_voice_profile",
|
|
459
|
+
path: "/sd/voice/profile",
|
|
460
|
+
description: "Build or fetch this user's personal writing-voice profile for one channel, mined ONLY from up to ~50 of their own SENT messages on that channel (never the other side of any conversation, never another user's data). Returns compact markdown: hard length stats (median/p75/p90 characters), 5-10 verbatim redacted exemplars, negative constraints, and language behavior. Computed once and cached; call with force_recompute:true only when explicitly asked to refresh. Call this before drafting on the user's behalf and match the returned stats and exemplars.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
channel: { type: "string", enum: ["linkedin", "whatsapp", "email"] },
|
|
465
|
+
force_recompute: { type: "boolean", default: false },
|
|
466
|
+
},
|
|
467
|
+
required: ["channel"],
|
|
468
|
+
additionalProperties: false,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "li_search_connections",
|
|
473
|
+
path: "/li/connections/search",
|
|
474
|
+
description: "Search only your locally stored LinkedIn connections by name, headline, or company and join exact local contact state. Makes zero LinkedIn, Unipile, or paid discovery API calls.",
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
query: { type: "string", minLength: 1, maxLength: 200 },
|
|
479
|
+
filters: {
|
|
480
|
+
type: "object",
|
|
481
|
+
properties: {
|
|
482
|
+
company: { type: "string", minLength: 1, maxLength: 200 },
|
|
483
|
+
headline_keyword: {
|
|
484
|
+
type: "string",
|
|
485
|
+
minLength: 1,
|
|
486
|
+
maxLength: 200,
|
|
487
|
+
},
|
|
488
|
+
connected_after: { type: "string", format: "date" },
|
|
489
|
+
connected_before: { type: "string", format: "date" },
|
|
490
|
+
},
|
|
491
|
+
additionalProperties: false,
|
|
492
|
+
},
|
|
493
|
+
limit: {
|
|
494
|
+
type: "integer",
|
|
495
|
+
minimum: 1,
|
|
496
|
+
maximum: 100,
|
|
497
|
+
default: 20,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
required: ["query"],
|
|
501
|
+
additionalProperties: false,
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "li_discover_people",
|
|
506
|
+
path: "/li/discover_people",
|
|
507
|
+
description: "Discover public LinkedIn profile cards beyond your completed stored network through one capped HarvestAPI page. Local matches always win. First preview the exact paid request without confirm, obtain approval, then repeat with confirm:true. Each preview authorizes one provider request.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
query: {
|
|
512
|
+
type: "string",
|
|
513
|
+
minLength: 1,
|
|
514
|
+
maxLength: 200,
|
|
515
|
+
description: "Role or title to search.",
|
|
516
|
+
},
|
|
517
|
+
filters: {
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
company: { type: "string", minLength: 1, maxLength: 200 },
|
|
521
|
+
location: { type: "string", minLength: 1, maxLength: 200 },
|
|
522
|
+
},
|
|
523
|
+
additionalProperties: false,
|
|
524
|
+
},
|
|
525
|
+
limit: {
|
|
526
|
+
type: "integer",
|
|
527
|
+
minimum: 1,
|
|
528
|
+
maximum: 10,
|
|
529
|
+
default: 10,
|
|
530
|
+
},
|
|
531
|
+
confirm: { type: "boolean", default: false },
|
|
532
|
+
},
|
|
533
|
+
required: ["query"],
|
|
534
|
+
additionalProperties: false,
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "li_create_invitation_batch",
|
|
539
|
+
path: "/li/invitation_batch/create",
|
|
540
|
+
description: "Create one immutable LinkedIn Classic invitation-batch preview from exact user-approved profile URLs, inclusion reasons, and optional notes. The agent structures the natural-language request; the server never generates targets or text. Maximum 10 targets.",
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
source_label: {
|
|
545
|
+
type: "string",
|
|
546
|
+
minLength: 1,
|
|
547
|
+
maxLength: 120,
|
|
548
|
+
},
|
|
549
|
+
time_zone: {
|
|
550
|
+
type: "string",
|
|
551
|
+
minLength: 1,
|
|
552
|
+
description: "Sender IANA timezone.",
|
|
553
|
+
},
|
|
554
|
+
targets: {
|
|
555
|
+
type: "array",
|
|
556
|
+
minItems: 1,
|
|
557
|
+
maxItems: 10,
|
|
558
|
+
items: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
profile_url: {
|
|
562
|
+
type: "string",
|
|
563
|
+
format: "uri",
|
|
564
|
+
description: "Exact LinkedIn Classic https://www.linkedin.com/in/... URL.",
|
|
565
|
+
},
|
|
566
|
+
inclusion_reason: {
|
|
567
|
+
type: "string",
|
|
568
|
+
minLength: 1,
|
|
569
|
+
maxLength: 240,
|
|
570
|
+
},
|
|
571
|
+
note: {
|
|
572
|
+
type: "string",
|
|
573
|
+
maxLength: 200,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
required: ["profile_url", "inclusion_reason"],
|
|
577
|
+
additionalProperties: false,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
required: ["source_label", "time_zone", "targets"],
|
|
582
|
+
additionalProperties: false,
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "li_get_invitation_batch",
|
|
587
|
+
path: "/li/invitation_batch/get",
|
|
588
|
+
description: "Inspect one exact immutable invitation batch, every target and exclusion, approval hash, timing, capacity, and execution result. This read authorizes a later exact cancel; MCP cannot approve.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: {
|
|
592
|
+
batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
593
|
+
},
|
|
594
|
+
required: ["batch_id"],
|
|
595
|
+
additionalProperties: false,
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "li_cancel_invitation_batch",
|
|
600
|
+
path: "/li/invitation_batch/cancel",
|
|
601
|
+
description: "Permanently cancel one freshly inspected invitation batch. Cancels only unstarted targets and cannot recall an executing invitation. Restart requires a new preview and human approval.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
606
|
+
approval_view_hash: {
|
|
607
|
+
type: ["string", "null"],
|
|
608
|
+
pattern: "^[0-9a-f]{64}$",
|
|
609
|
+
},
|
|
610
|
+
confirm: { type: "boolean" },
|
|
611
|
+
},
|
|
612
|
+
required: ["batch_id", "approval_view_hash", "confirm"],
|
|
613
|
+
additionalProperties: false,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "sd_campaign_create",
|
|
618
|
+
path: "/sd/campaign/create",
|
|
619
|
+
description: "Create one campaign: a paced connection request to each exact person, then the exact approved message(s) once that person is PROVEN to have accepted, then an optional follow-up that stops the moment they reply. Nothing is sent until a human approves this exact recipient list and this exact message text on the approval page. Targets come from an explicit list you supply (for example one you built with li_search_connections or li_post_reactions) or from your own post engagers, which costs zero profile fetches. If the user already sent someone a connection request by hand and just wants the follow-up automated, set adopt_existing_invitation on that target instead of leaving them out. Draft the messages in the user's own voice and keep them short: the on-acceptance group is 2-3 separate short sends, never one block.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: "object",
|
|
622
|
+
properties: {
|
|
623
|
+
source_label: { type: "string", minLength: 1, maxLength: 120 },
|
|
624
|
+
time_zone: {
|
|
625
|
+
type: "string",
|
|
626
|
+
minLength: 1,
|
|
627
|
+
description: "Sender IANA timezone. Execution is Mon-Fri 09:00-17:00 sender-local.",
|
|
628
|
+
},
|
|
629
|
+
target_source: {
|
|
630
|
+
type: "string",
|
|
631
|
+
enum: ["explicit", "post_engagers"],
|
|
632
|
+
default: "explicit",
|
|
633
|
+
},
|
|
634
|
+
targets: {
|
|
635
|
+
type: "array",
|
|
636
|
+
minItems: 1,
|
|
637
|
+
maxItems: 150,
|
|
638
|
+
description: "Explicit targets. Each needs a canonical linkedin.com/in/ URL or an exact provider_id.",
|
|
639
|
+
items: {
|
|
640
|
+
type: "object",
|
|
641
|
+
properties: {
|
|
642
|
+
profile_url: { type: "string", format: "uri" },
|
|
643
|
+
provider_id: { type: "string", minLength: 5, maxLength: 500 },
|
|
644
|
+
inclusion_reason: { type: "string", minLength: 1, maxLength: 240 },
|
|
645
|
+
note: {
|
|
646
|
+
type: "string",
|
|
647
|
+
maxLength: 200,
|
|
648
|
+
description: "Optional invitation note.",
|
|
649
|
+
},
|
|
650
|
+
display_name: { type: "string", maxLength: 160 },
|
|
651
|
+
headline: { type: "string", maxLength: 300 },
|
|
652
|
+
adopt_existing_invitation: {
|
|
653
|
+
type: "boolean",
|
|
654
|
+
default: false,
|
|
655
|
+
description: "Set true ONLY when the user already sent this exact person a connection request by hand and now wants the follow-up automated. SignalDash then REQUIRES a still-pending sent invitation to that person and sends none of its own: it adopts the existing one, watches for acceptance, and runs the message sequence. If no pending invitation is found the person is dropped, never invited. Incompatible with note, because no invitation goes out.",
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
required: ["inclusion_reason"],
|
|
659
|
+
additionalProperties: false,
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
engagers: {
|
|
663
|
+
type: "object",
|
|
664
|
+
description: "Only with target_source post_engagers. The server reads your own recent posts and their reactions and comments, which return member id and network distance directly, so no profiles are fetched.",
|
|
665
|
+
properties: {
|
|
666
|
+
post_limit: { type: "integer", minimum: 1, maximum: 10, default: 5 },
|
|
667
|
+
max_targets: { type: "integer", minimum: 1, maximum: 150, default: 50 },
|
|
668
|
+
inclusion_reason: { type: "string", minLength: 1, maxLength: 240 },
|
|
669
|
+
note: { type: "string", maxLength: 200 },
|
|
670
|
+
},
|
|
671
|
+
additionalProperties: false,
|
|
672
|
+
},
|
|
673
|
+
messages: {
|
|
674
|
+
type: "array",
|
|
675
|
+
minItems: 1,
|
|
676
|
+
maxItems: 5,
|
|
677
|
+
description: "The exact frozen texts. after_days 0 means sent once the invitation is accepted (1-3 of these, sent as separate consecutive messages); a later step needs after_days 1-30 and only goes out if there has been no reply.",
|
|
678
|
+
items: {
|
|
679
|
+
type: "object",
|
|
680
|
+
properties: {
|
|
681
|
+
text: { type: "string", minLength: 1, maxLength: 1200 },
|
|
682
|
+
after_days: { type: "integer", minimum: 0, maximum: 30 },
|
|
683
|
+
},
|
|
684
|
+
required: ["text"],
|
|
685
|
+
additionalProperties: false,
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
invite_ttl_days: {
|
|
689
|
+
type: "integer",
|
|
690
|
+
minimum: 1,
|
|
691
|
+
maximum: 60,
|
|
692
|
+
default: 21,
|
|
693
|
+
description: "An invitation not accepted within this many days is dropped and never messaged.",
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
required: ["source_label", "time_zone", "messages"],
|
|
697
|
+
additionalProperties: false,
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: "sd_campaign_preview",
|
|
702
|
+
path: "/sd/campaign/preview",
|
|
703
|
+
description: "Inspect one campaign before approval: every exact recipient, every exclusion and its reason, the exact message text for each step, the timing, the shared daily budget, and the approval_url to send the human. Show the human this content. This read also authorizes a later exact cancel. An agent cannot approve a campaign; only the authenticated human page can, and it can hand you a one-time code for sd_campaign_approve.",
|
|
704
|
+
inputSchema: {
|
|
705
|
+
type: "object",
|
|
706
|
+
properties: {
|
|
707
|
+
campaign_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
708
|
+
},
|
|
709
|
+
required: ["campaign_id"],
|
|
710
|
+
additionalProperties: false,
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: "sd_campaign_approve",
|
|
715
|
+
path: "/sd/campaign/approve",
|
|
716
|
+
description: "Record the human's approval of one campaign using the one-time code they generated on the authenticated approval page. You cannot create that code, guess it, or approve without it, and it only ever approves the exact payload and exact recipients they reviewed.",
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: "object",
|
|
719
|
+
properties: {
|
|
720
|
+
campaign_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
721
|
+
confirm_token: {
|
|
722
|
+
type: "string",
|
|
723
|
+
pattern: "^sd-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$",
|
|
724
|
+
description: "The code the human read off the approval page.",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
required: ["campaign_id", "confirm_token"],
|
|
728
|
+
additionalProperties: false,
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "sd_campaign_status",
|
|
733
|
+
path: "/sd/campaign/status",
|
|
734
|
+
description: "Monitor one campaign: invited, acceptance proven, messaged, replied and halted, expired, or excluded per person, plus every message step and any provider response SignalDash could not parse.",
|
|
735
|
+
inputSchema: {
|
|
736
|
+
type: "object",
|
|
737
|
+
properties: {
|
|
738
|
+
campaign_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
739
|
+
},
|
|
740
|
+
required: ["campaign_id"],
|
|
741
|
+
additionalProperties: false,
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "sd_campaign_cancel",
|
|
746
|
+
path: "/sd/campaign/cancel",
|
|
747
|
+
description: "Revoke the human approval and cancel one freshly inspected campaign. Stops every unsent invitation and every unsent message. Cannot recall an executing write. Restarting requires a new preview and a new human approval.",
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
campaign_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
752
|
+
approval_view_hash: {
|
|
753
|
+
type: ["string", "null"],
|
|
754
|
+
pattern: "^[0-9a-f]{64}$",
|
|
755
|
+
},
|
|
756
|
+
confirm: { type: "boolean" },
|
|
757
|
+
},
|
|
758
|
+
required: ["campaign_id", "approval_view_hash", "confirm"],
|
|
759
|
+
additionalProperties: false,
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: "sd_withdrawal_batch_create",
|
|
764
|
+
path: "/li/withdrawal_batch/create",
|
|
765
|
+
description:
|
|
766
|
+
"Clear a backlog of old PENDING SENT LinkedIn invitations. Freezes the exact invitation ids matching an age rule, then one human approval withdraws them one at a time. Two things you must tell the user, because both are LinkedIn's own documented behaviour: (1) this does NOT free up sending capacity, withdrawing does not lift an active sending restriction, it only clears a stale backlog; (2) after withdrawing you cannot re-invite that person for UP TO THREE WEEKS. `exclude` is REQUIRED, not optional: ask the user who must NOT be withdrawn before you call this, because a blanket age rule will otherwise catch people they wanted to keep pending. Passing an empty array is allowed but it means nobody is protected. Age labels are buckets ('sent 4 months ago'), not dates, so the filter is bucket-accurate at best and deliberately holds back anyone whose bucket straddles the threshold.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
account: {
|
|
771
|
+
type: "string",
|
|
772
|
+
enum: ["linkedin"],
|
|
773
|
+
description: "Only LinkedIn sent invitations can be swept.",
|
|
774
|
+
},
|
|
775
|
+
older_than_days: {
|
|
776
|
+
type: "integer",
|
|
777
|
+
minimum: 30,
|
|
778
|
+
maximum: 3650,
|
|
779
|
+
default: 90,
|
|
780
|
+
description:
|
|
781
|
+
"Withdraw invitations older than this. Bucket-accurate at best. Minimum 30.",
|
|
782
|
+
},
|
|
783
|
+
exclude: {
|
|
784
|
+
type: "array",
|
|
785
|
+
maxItems: 100,
|
|
786
|
+
description:
|
|
787
|
+
"REQUIRED. People to protect from the sweep, matched exactly, never fuzzily. Ask the user for these by name before calling. An empty array means nobody is protected.",
|
|
788
|
+
items: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {
|
|
791
|
+
kind: {
|
|
792
|
+
type: "string",
|
|
793
|
+
enum: [
|
|
794
|
+
"invitation_id",
|
|
795
|
+
"provider_id",
|
|
796
|
+
"public_identifier",
|
|
797
|
+
"profile_url",
|
|
798
|
+
"member_urn",
|
|
799
|
+
"display_name",
|
|
800
|
+
],
|
|
801
|
+
},
|
|
802
|
+
value: { type: "string", minLength: 1, maxLength: 1000 },
|
|
803
|
+
},
|
|
804
|
+
required: ["kind", "value"],
|
|
805
|
+
additionalProperties: false,
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
time_zone: {
|
|
809
|
+
type: "string",
|
|
810
|
+
minLength: 1,
|
|
811
|
+
maxLength: 64,
|
|
812
|
+
description:
|
|
813
|
+
"IANA sender timezone. Withdrawals only run Monday-Friday 09:00-17:00 in this zone.",
|
|
814
|
+
},
|
|
815
|
+
limit: {
|
|
816
|
+
type: "integer",
|
|
817
|
+
minimum: 1,
|
|
818
|
+
maximum: 1500,
|
|
819
|
+
description: "Optional ceiling on how many to withdraw.",
|
|
820
|
+
},
|
|
821
|
+
source_label: { type: "string", minLength: 1, maxLength: 120 },
|
|
822
|
+
allow_unmatched_exclusions: {
|
|
823
|
+
type: "boolean",
|
|
824
|
+
description:
|
|
825
|
+
"By default an exclusion that matches nobody fails the preview, because that is what a typo looks like and a typo means the protected person gets withdrawn. Set true only when the user confirms the mismatch is expected.",
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
required: ["account", "exclude", "time_zone"],
|
|
829
|
+
additionalProperties: false,
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "sd_withdrawal_batch_status",
|
|
834
|
+
path: "/li/withdrawal_batch/status",
|
|
835
|
+
description:
|
|
836
|
+
"Inspect or monitor one withdrawal sweep: how many are done and remaining, the per-day pace and this sweep's own daily allowance (it never consumes your send budget), the stop reason if it stopped, any provider response SignalDash could not parse, the exact people your exclusions protected, and the approval_url to send the human. This read also authorizes a later exact cancel.",
|
|
837
|
+
inputSchema: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
withdrawal_batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
841
|
+
},
|
|
842
|
+
required: ["withdrawal_batch_id"],
|
|
843
|
+
additionalProperties: false,
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
name: "sd_withdrawal_batch_approve",
|
|
848
|
+
path: "/li/withdrawal_batch/approve",
|
|
849
|
+
description:
|
|
850
|
+
"Record the human's approval of one withdrawal sweep using the one-time code they generated on the authenticated approval page. You cannot create that code, guess it, or approve without it, and it only ever approves the exact invitations they left ticked.",
|
|
851
|
+
inputSchema: {
|
|
852
|
+
type: "object",
|
|
853
|
+
properties: {
|
|
854
|
+
withdrawal_batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
855
|
+
confirm_token: {
|
|
856
|
+
type: "string",
|
|
857
|
+
pattern: "^sd-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}$",
|
|
858
|
+
description: "The code the human read off the approval page.",
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
required: ["withdrawal_batch_id", "confirm_token"],
|
|
862
|
+
additionalProperties: false,
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: "sd_withdrawal_batch_cancel",
|
|
867
|
+
path: "/li/withdrawal_batch/cancel",
|
|
868
|
+
description:
|
|
869
|
+
"Revoke the human approval and stop one freshly inspected withdrawal sweep. Stops every withdrawal that has not started. Cannot recall one already submitted, and cannot restore the three-week re-invite block for people already withdrawn.",
|
|
870
|
+
inputSchema: {
|
|
871
|
+
type: "object",
|
|
872
|
+
properties: {
|
|
873
|
+
withdrawal_batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
874
|
+
approval_view_hash: {
|
|
875
|
+
type: ["string", "null"],
|
|
876
|
+
pattern: "^[0-9a-f]{64}$",
|
|
877
|
+
},
|
|
878
|
+
confirm: { type: "boolean" },
|
|
879
|
+
},
|
|
880
|
+
required: ["withdrawal_batch_id", "approval_view_hash", "confirm"],
|
|
881
|
+
additionalProperties: false,
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: "wa_list_chats",
|
|
886
|
+
ch: "wa",
|
|
887
|
+
action: "list_chats",
|
|
888
|
+
description: "List or search your WhatsApp chats. `search` matches the phone number (chats carry it as attendee_public_identifier), not a person name; the response reports how far it scanned.",
|
|
889
|
+
inputSchema: {
|
|
890
|
+
type: "object",
|
|
891
|
+
properties: {
|
|
892
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "How many chats to return. With `search`, how many MATCHES to return." },
|
|
893
|
+
cursor: { type: "string", maxLength: 4000, description: "Continue from a previous page or a previous search." },
|
|
894
|
+
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on name and counterpart identifiers. The provider cannot filter a chat list, so SignalDash filters over a bounded scan and the response reports `scanned_chats` and `exhaustive` so a miss is never mistaken for a proven absence. A 1:1 chat has no name, so search by identifier or phone number, not by person name." },
|
|
895
|
+
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "How many chats the search may scan before it stops and reports exhaustive:false. Default 200, and never more than five provider pages: bulk reading is the top account-restriction trigger, so go deeper with the returned cursor rather than with a bigger scan." },
|
|
896
|
+
},
|
|
897
|
+
additionalProperties: false,
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "wa_read_messages",
|
|
902
|
+
ch: "wa",
|
|
903
|
+
action: "read",
|
|
904
|
+
description: "Read messages in a WhatsApp chat. args: chat_id",
|
|
905
|
+
inputSchema: {
|
|
906
|
+
type: "object",
|
|
907
|
+
properties: {
|
|
908
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
909
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
910
|
+
},
|
|
911
|
+
required: ["chat_id"],
|
|
912
|
+
additionalProperties: false,
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "wa_get_attachment",
|
|
917
|
+
path: "/wa/get_attachment",
|
|
918
|
+
description: "Download one attachment of one WhatsApp message this account owns and store it on the SignalDash host. Read the chat first to obtain the exact message_id and attachment_id. Returns the stored path, mimetype, byte size and sha256.",
|
|
919
|
+
inputSchema: {
|
|
920
|
+
type: "object",
|
|
921
|
+
properties: {
|
|
922
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
923
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
924
|
+
attachment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
925
|
+
},
|
|
926
|
+
required: ["chat_id", "message_id", "attachment_id"],
|
|
927
|
+
additionalProperties: false,
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "wa_transcribe_voice",
|
|
932
|
+
path: "/wa/transcribe_voice",
|
|
933
|
+
description: "Transcribe one WhatsApp voice note or audio attachment this account owns. Fetches the audio through SignalDash, transcribes it on the SignalDash host, and returns the transcript text, the stored audio path, and the `backend` that produced the text. Optional `backend` picks the engine: `gemini` (default, accurate on German with English terms mixed in) or `whisper` (local small model, much weaker on code-switching). A `gemini` request that cannot reach Gemini falls back to the local model and says so in `backend` and `fallback_reason`.",
|
|
934
|
+
inputSchema: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
938
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
939
|
+
attachment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
940
|
+
backend: { type: "string", enum: ["gemini", "whisper"] },
|
|
941
|
+
},
|
|
942
|
+
required: ["chat_id", "message_id", "attachment_id"],
|
|
943
|
+
additionalProperties: false,
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: "wa_send_message",
|
|
948
|
+
ch: "wa",
|
|
949
|
+
action: "send",
|
|
950
|
+
description: "Send a WhatsApp message (rate-safe). args: chat_id, text, attachments. Read the chat first: a send into a thread this account has not read recently is refused. `attachments` optionally carries up to 4 base64 files as exact {filename, content_type, content_base64} objects, at most 16 MiB per file and 16 MiB per message, and accepts images, PDF, CSV, plain text, JSON, xlsx and zip. `text` is the caption and may be omitted when a file is attached, but a call carrying neither text nor an attachment is refused. An attachment send is rate-limited, deduplicated and recorded exactly like a text send, and spends the same daily budget. If a send times out or the provider never confirms it, the message may still have been delivered: SignalDash records it and refuses an identical retry with `409 send_outcome_unknown`. Read the chat, and only if the message is genuinely absent, resend the identical payload with `confirm_resend:true`.",
|
|
951
|
+
inputSchema: {
|
|
952
|
+
type: "object",
|
|
953
|
+
properties: {
|
|
954
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
955
|
+
text: { type: "string", minLength: 1, maxLength: 5000 },
|
|
956
|
+
attachments: {
|
|
957
|
+
type: "array", maxItems: 4,
|
|
958
|
+
items: {
|
|
959
|
+
type: "object",
|
|
960
|
+
properties: {
|
|
961
|
+
filename: { type: "string", minLength: 1, maxLength: 180 },
|
|
962
|
+
content_type: {
|
|
963
|
+
type: "string",
|
|
964
|
+
enum: [
|
|
965
|
+
"image/png",
|
|
966
|
+
"image/jpeg",
|
|
967
|
+
"image/webp",
|
|
968
|
+
"image/gif",
|
|
969
|
+
"application/pdf",
|
|
970
|
+
"text/csv",
|
|
971
|
+
"text/plain",
|
|
972
|
+
"application/json",
|
|
973
|
+
"application/zip",
|
|
974
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
975
|
+
],
|
|
976
|
+
},
|
|
977
|
+
content_base64: { type: "string", minLength: 1 },
|
|
978
|
+
},
|
|
979
|
+
required: ["filename", "content_type", "content_base64"],
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
confirm_resend: { type: "boolean", const: true },
|
|
983
|
+
},
|
|
984
|
+
// `text` alone is no longer required: a document with no caption is a
|
|
985
|
+
// legitimate message. `anyOf` states the real rule to any client that
|
|
986
|
+
// validates locally, and the backend refuses a call carrying neither
|
|
987
|
+
// regardless of whether the client checked.
|
|
988
|
+
required: ["chat_id"],
|
|
989
|
+
anyOf: [{ required: ["text"] }, { required: ["attachments"] }],
|
|
990
|
+
additionalProperties: false,
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
name: "wa_delete_message",
|
|
995
|
+
path: "/wa/delete_message",
|
|
996
|
+
description: "Delete one WhatsApp message this account SENT, in a chat this account owns. Read the chat first: the exact `message_id` comes from `wa_read_messages`. Only your own messages can be deleted; someone else's is refused with `403 message_not_own`. This is irreversible and is never retried: a delete already recorded for this exact chat and message is refused with `409 duplicate_delete` rather than replayed. WhatsApp applies its own time and role limits to deleting for everyone and can answer successfully without removing anything, so re-read the chat afterwards to confirm. Deletes spend their own daily budget and never consume your send budget.",
|
|
997
|
+
inputSchema: {
|
|
998
|
+
type: "object",
|
|
999
|
+
properties: {
|
|
1000
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1001
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1002
|
+
},
|
|
1003
|
+
required: ["chat_id", "message_id"],
|
|
1004
|
+
additionalProperties: false,
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
name: "wa_delete_messages",
|
|
1009
|
+
path: "/wa/delete_messages",
|
|
1010
|
+
description: "Delete several WhatsApp messages this account sent, at most 200 per call. Every entry runs the exact same ownership, budget and audit path as `wa_delete_message`, one at a time with a pause between them, never in parallel. Returns a per-entry `ok` with the refusal `code` and `error` for each one that did not go through, so a partial result is readable rather than all-or-nothing. Entries the batch never reached before its time limit come back with `skipped:true` and `code:batch_deadline`; resend exactly those to resume.",
|
|
1011
|
+
inputSchema: {
|
|
1012
|
+
type: "object",
|
|
1013
|
+
properties: {
|
|
1014
|
+
messages: {
|
|
1015
|
+
type: "array",
|
|
1016
|
+
minItems: 1,
|
|
1017
|
+
maxItems: 200,
|
|
1018
|
+
items: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
properties: {
|
|
1021
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1022
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1023
|
+
},
|
|
1024
|
+
required: ["chat_id", "message_id"],
|
|
1025
|
+
additionalProperties: false,
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
required: ["messages"],
|
|
1030
|
+
additionalProperties: false,
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
196
1033
|
{
|
|
197
1034
|
name: "email_list",
|
|
198
1035
|
path: "/email/list",
|
|
@@ -200,6 +1037,7 @@ const TOOLS = [
|
|
|
200
1037
|
inputSchema: {
|
|
201
1038
|
type: "object",
|
|
202
1039
|
properties: { limit: { type: "integer", minimum: 1, maximum: 100 } },
|
|
1040
|
+
additionalProperties: false,
|
|
203
1041
|
},
|
|
204
1042
|
},
|
|
205
1043
|
{
|
|
@@ -213,6 +1051,7 @@ const TOOLS = [
|
|
|
213
1051
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
214
1052
|
},
|
|
215
1053
|
required: ["thread_id"],
|
|
1054
|
+
additionalProperties: false,
|
|
216
1055
|
},
|
|
217
1056
|
},
|
|
218
1057
|
{
|
|
@@ -232,17 +1071,146 @@ const TOOLS = [
|
|
|
232
1071
|
body: { type: "string", minLength: 1, maxLength: 5000 },
|
|
233
1072
|
},
|
|
234
1073
|
required: ["to", "subject", "body"],
|
|
1074
|
+
additionalProperties: false,
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: "li_my_posts",
|
|
1079
|
+
path: "/li/posts",
|
|
1080
|
+
description: "List the user's own LinkedIn posts with engagement counts (reactions, comments, impressions). args: limit",
|
|
1081
|
+
inputSchema: {
|
|
1082
|
+
type: "object",
|
|
1083
|
+
properties: {
|
|
1084
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
1085
|
+
member_id: { type: "string", minLength: 1, maxLength: 500, description: "Whose posts to list. Defaults to your own." },
|
|
1086
|
+
},
|
|
1087
|
+
additionalProperties: false,
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
name: "li_post_reactions",
|
|
1092
|
+
path: "/li/post_reactions",
|
|
1093
|
+
description: "Who reacted to a post — name + headline. These are warm inbound signals. args: post_id, limit",
|
|
1094
|
+
inputSchema: {
|
|
1095
|
+
type: "object",
|
|
1096
|
+
properties: {
|
|
1097
|
+
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1098
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1099
|
+
},
|
|
1100
|
+
required: ["post_id"],
|
|
1101
|
+
additionalProperties: false,
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
name: "li_post_comments",
|
|
1106
|
+
path: "/li/post_comments",
|
|
1107
|
+
description: "Comments on a post, with author. args: post_id, limit",
|
|
1108
|
+
inputSchema: {
|
|
1109
|
+
type: "object",
|
|
1110
|
+
properties: {
|
|
1111
|
+
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1112
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1113
|
+
},
|
|
1114
|
+
required: ["post_id"],
|
|
1115
|
+
additionalProperties: false,
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "li_draft_post",
|
|
1120
|
+
path: "/li/create_post",
|
|
1121
|
+
description: "Draft, publish, or schedule a LinkedIn post. Scheduling requires an offset-qualified scheduled_at plus publish:true after exact human approval. Optional mentions, base64 image attachments, and an account-owner-authored first_comment are preserved for the scheduled publish.",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
type: "object",
|
|
1124
|
+
properties: {
|
|
1125
|
+
text: { type: "string", minLength: 1, maxLength: 3000 },
|
|
1126
|
+
publish: { type: "boolean" },
|
|
1127
|
+
scheduled_at: { type: "string", format: "date-time" },
|
|
1128
|
+
first_comment: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1129
|
+
mentions: {
|
|
1130
|
+
type: "array", maxItems: 20,
|
|
1131
|
+
items: {
|
|
1132
|
+
type: "object",
|
|
1133
|
+
properties: {
|
|
1134
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
1135
|
+
profile_id: { type: "string", minLength: 1, maxLength: 250 },
|
|
1136
|
+
},
|
|
1137
|
+
required: ["name", "profile_id"],
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
attachments: {
|
|
1141
|
+
type: "array", maxItems: 4,
|
|
1142
|
+
items: {
|
|
1143
|
+
type: "object",
|
|
1144
|
+
properties: {
|
|
1145
|
+
filename: { type: "string", minLength: 1, maxLength: 180 },
|
|
1146
|
+
content_type: { type: "string", enum: ["image/png", "image/jpeg", "image/webp", "image/gif"] },
|
|
1147
|
+
content_base64: { type: "string", minLength: 1 },
|
|
1148
|
+
},
|
|
1149
|
+
required: ["filename", "content_type", "content_base64"],
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
required: ["text"],
|
|
1154
|
+
additionalProperties: false,
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
name: "li_set_scheduled_post_first_comment",
|
|
1159
|
+
path: "/li/set_scheduled_post_first_comment",
|
|
1160
|
+
description: "Attach one exact approved first comment to an existing scheduled LinkedIn post. SignalDash publishes it through the same connected account immediately after the post. Requires id, first_comment, and confirm:true.",
|
|
1161
|
+
inputSchema: {
|
|
1162
|
+
type: "object",
|
|
1163
|
+
properties: {
|
|
1164
|
+
id: { type: "string", format: "uuid" },
|
|
1165
|
+
first_comment: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1166
|
+
confirm: { type: "boolean", const: true },
|
|
1167
|
+
},
|
|
1168
|
+
required: ["id", "first_comment", "confirm"],
|
|
1169
|
+
additionalProperties: false,
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
name: "li_scheduled_posts",
|
|
1174
|
+
path: "/li/scheduled_posts",
|
|
1175
|
+
description: "List this authenticated user's scheduled LinkedIn posts and their current states.",
|
|
1176
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
name: "li_cancel_scheduled_post",
|
|
1180
|
+
path: "/li/cancel_scheduled_post",
|
|
1181
|
+
description: "Cancel one exact scheduled LinkedIn post before execution. Requires id and confirm:true.",
|
|
1182
|
+
inputSchema: {
|
|
1183
|
+
type: "object",
|
|
1184
|
+
properties: {
|
|
1185
|
+
id: { type: "string", format: "uuid" },
|
|
1186
|
+
confirm: { type: "boolean", const: true },
|
|
1187
|
+
},
|
|
1188
|
+
required: ["id", "confirm"],
|
|
1189
|
+
additionalProperties: false,
|
|
235
1190
|
},
|
|
236
1191
|
},
|
|
237
|
-
{ name: "li_my_posts", path: "/li/posts", description: "List the user's own LinkedIn posts with engagement counts (reactions, comments, impressions). args: limit" },
|
|
238
|
-
{ name: "li_post_reactions", path: "/li/post_reactions", description: "Who reacted to a post — name + headline. These are warm inbound signals. args: post_id, limit" },
|
|
239
|
-
{ name: "li_post_comments", path: "/li/post_comments", description: "Comments on a post, with author. args: post_id, limit" },
|
|
240
|
-
{ name: "li_draft_post", path: "/li/create_post", description: "Draft a LinkedIn post. Returns the draft WITHOUT publishing. Publishing requires the human to approve and re-send with publish:true. args: text" },
|
|
241
1192
|
];
|
|
1193
|
+
// No catch-all fallback. It advertised one union of keys for every tool that
|
|
1194
|
+
// had no schema of its own -- which is how `li_list_chats` came to offer `text`
|
|
1195
|
+
// and `member_id` that the route never reads, and how a caller could not tell a
|
|
1196
|
+
// filtered search from an unfiltered list.
|
|
1197
|
+
//
|
|
1198
|
+
// A tool with no schema is dropped from the listing rather than shipped with a
|
|
1199
|
+
// promise the backend does not keep. Dropped, not thrown: tools/list maps every
|
|
1200
|
+
// tool through here, so throwing would take the whole LinkedIn AND WhatsApp
|
|
1201
|
+
// surface down over one bad entry. The test suite is what fails on a missing
|
|
1202
|
+
// schema; the live surface degrades by exactly one tool.
|
|
242
1203
|
function mcpTool(name) {
|
|
243
1204
|
const tool = TOOLS.find(t => t.name === name);
|
|
244
|
-
|
|
245
|
-
|
|
1205
|
+
if (!tool || !tool.inputSchema) {
|
|
1206
|
+
process.stderr.write(`[signaldash] tool ${name} has no inputSchema; omitted\n`);
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
name,
|
|
1211
|
+
description: tool.description,
|
|
1212
|
+
inputSchema: tool.inputSchema,
|
|
1213
|
+
};
|
|
246
1214
|
}
|
|
247
1215
|
export async function runMcp(dependencies = {}) {
|
|
248
1216
|
const input = dependencies.input || process.stdin;
|
|
@@ -253,8 +1221,8 @@ export async function runMcp(dependencies = {}) {
|
|
|
253
1221
|
for await (const line of rl) {
|
|
254
1222
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
255
1223
|
const { id, method, params } = msg;
|
|
256
|
-
if (method === "initialize") reply(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "signaldash", version:
|
|
257
|
-
else if (method === "tools/list") reply(id, { tools: TOOLS.map(t => mcpTool(t.name)) });
|
|
1224
|
+
if (method === "initialize") reply(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "signaldash", version: PACKAGE_VERSION } });
|
|
1225
|
+
else if (method === "tools/list") reply(id, { tools: TOOLS.map(t => mcpTool(t.name)).filter(Boolean) });
|
|
258
1226
|
else if (method === "tools/call") {
|
|
259
1227
|
const t = TOOLS.find(x => x.name === params.name);
|
|
260
1228
|
if (!t) { reply(id, null, { code: -32601, message: "unknown tool" }); continue; }
|