@floomhq/signaldash 0.1.0 → 0.3.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 +74 -73
- package/bin/sd.mjs +121 -31
- package/package.json +2 -2
- package/skills/signaldash/SKILL.md +68 -0
package/README.md
CHANGED
|
@@ -2,59 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
Secure LinkedIn and WhatsApp access for AI agents.
|
|
4
4
|
|
|
5
|
-
`@floomhq/signaldash` connects your
|
|
6
|
-
six account-scoped MCP tools.
|
|
7
|
-
|
|
5
|
+
`@floomhq/signaldash` connects your accounts through the hosted SignalDash
|
|
6
|
+
service and exposes six account-scoped MCP tools. The CLI never receives or
|
|
7
|
+
stores the server's Unipile access key.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Quickstart
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
your DSN and generate an Access Token. The API URL is your DSN followed by
|
|
14
|
-
`/api/v1`.
|
|
11
|
+
You need Node.js 20 or newer and a single-use SignalDash invite code. Ask the
|
|
12
|
+
SignalDash administrator for an invite code, then run:
|
|
15
13
|
|
|
16
14
|
```bash
|
|
17
|
-
|
|
18
|
-
export SIGNALDASH_UNIPILE_KEY="..."
|
|
19
|
-
|
|
15
|
+
npx -y @floomhq/signaldash login <invite-code>
|
|
20
16
|
npx -y @floomhq/signaldash connect linkedin
|
|
21
17
|
npx -y @floomhq/signaldash connect whatsapp
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
Each command
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
stores its account ID.
|
|
29
|
-
|
|
30
|
-
Without environment variables, run either command in a terminal. SignalDash
|
|
31
|
-
prompts for the API URL and hides API-key input.
|
|
32
|
-
|
|
33
|
-
Credentials live under `~/.signaldash` by default:
|
|
20
|
+
Each connect command prints a short-lived hosted-auth URL and waits for the
|
|
21
|
+
connection to finish. Open the LinkedIn URL and sign in, or open the WhatsApp
|
|
22
|
+
URL and scan the live QR code. The CLI prints `Connected linkedin` or
|
|
23
|
+
`Connected whatsapp` when the account is ready.
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
The invite code is single-use. Login stores a SignalDash user token in
|
|
26
|
+
`~/.signaldash/config.json`; the Unipile access key remains on the SignalDash
|
|
27
|
+
server. Set `SIGNALDASH_HOME` to keep the local config in another directory.
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## MCP
|
|
29
|
+
If automatic account detection times out, the CLI prints the manual claim
|
|
30
|
+
command for the provider:
|
|
43
31
|
|
|
44
32
|
```bash
|
|
45
|
-
npx -y @floomhq/signaldash
|
|
33
|
+
npx -y @floomhq/signaldash connect linkedin claim <account_id>
|
|
46
34
|
```
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
## Add SignalDash to your agent
|
|
37
|
+
|
|
38
|
+
### Claude Code
|
|
50
39
|
|
|
51
|
-
|
|
40
|
+
Paste this command into a terminal:
|
|
52
41
|
|
|
53
42
|
```bash
|
|
54
43
|
claude mcp add signaldash -- npx -y @floomhq/signaldash mcp
|
|
55
44
|
```
|
|
56
45
|
|
|
57
|
-
Cursor
|
|
46
|
+
### Cursor
|
|
47
|
+
|
|
48
|
+
Create or update `.cursor/mcp.json` in the project. Merge the `signaldash`
|
|
49
|
+
entry into an existing `mcpServers` object when the file already contains
|
|
50
|
+
other servers:
|
|
58
51
|
|
|
59
52
|
```json
|
|
60
53
|
{
|
|
@@ -67,49 +60,57 @@ Cursor, `.cursor/mcp.json`:
|
|
|
67
60
|
}
|
|
68
61
|
```
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
Both registrations start the same stdio server:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y @floomhq/signaldash mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The MCP server uses the user token created by `login`. It cannot access a
|
|
70
|
+
LinkedIn or WhatsApp account until that channel has been connected for the
|
|
71
|
+
same logged-in user.
|
|
72
|
+
|
|
73
|
+
## Give the agent the safety skill
|
|
74
|
+
|
|
75
|
+
Before allowing the agent to send messages, give it the bundled
|
|
76
|
+
[`signaldash-safe-usage` skill](skills/signaldash-safe-usage/SKILL.md). The
|
|
77
|
+
skill requires the agent to resolve the exact chat, read recent thread
|
|
78
|
+
history, check for duplicates, and obtain approval when the recipient or text
|
|
79
|
+
has not already been approved.
|
|
80
|
+
|
|
81
|
+
The MCP registration does not automatically load the skill yet. Copy
|
|
82
|
+
`skills/signaldash-safe-usage/SKILL.md` into the agent's skills directory or
|
|
83
|
+
attach its contents to the agent instructions. Keep this requirement in both
|
|
84
|
+
Claude Code and Cursor.
|
|
85
|
+
|
|
86
|
+
## MCP tools
|
|
87
|
+
|
|
88
|
+
SignalDash exposes:
|
|
71
89
|
|
|
72
90
|
- `li_list_chats`
|
|
73
|
-
- `li_read_messages(
|
|
74
|
-
- `li_send_message(
|
|
91
|
+
- `li_read_messages(chat_id)`
|
|
92
|
+
- `li_send_message(chat_id, text)`
|
|
75
93
|
- `wa_list_chats`
|
|
76
|
-
- `wa_read_messages(
|
|
77
|
-
- `wa_send_message(
|
|
78
|
-
|
|
79
|
-
Every
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Safety
|
|
83
|
-
|
|
84
|
-
The
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- Provider warning headers, checkpoints, restrictions, HTTP 403, and HTTP 429
|
|
97
|
-
fail closed and persistently disable sends for that account.
|
|
98
|
-
- A file lock serializes sends across concurrent MCP processes.
|
|
99
|
-
|
|
100
|
-
LinkedIn invitation ceilings vary by account and often appear around 100 per
|
|
101
|
-
week. SignalDash exposes no invitation tool. Keep invitation automation out
|
|
102
|
-
of agent loops.
|
|
103
|
-
|
|
104
|
-
Avoid bulk profile reads, copied message bursts, cold-account automation, and
|
|
105
|
-
repeated retries after a checkpoint. Let the agent act slowly and human-like.
|
|
106
|
-
On WhatsApp, use established accounts, keep the phone online, and avoid bulk
|
|
107
|
-
or unsolicited messaging.
|
|
108
|
-
|
|
109
|
-
After a provider warning, stop. Review the provider account manually and wait
|
|
110
|
-
for it to return to good standing. The warning lock is recorded in
|
|
111
|
-
`~/.signaldash/safety/send-state.json`; only a human may clear the relevant
|
|
112
|
-
account entry after resolving the restriction.
|
|
94
|
+
- `wa_read_messages(chat_id)`
|
|
95
|
+
- `wa_send_message(chat_id, text)`
|
|
96
|
+
|
|
97
|
+
Every operation runs through the hosted SignalDash backend. Agents never
|
|
98
|
+
receive the Unipile access key.
|
|
99
|
+
|
|
100
|
+
## Safety limits
|
|
101
|
+
|
|
102
|
+
SignalDash enforces a daily send cap. The safe-usage skill adds the human
|
|
103
|
+
workflow around that runtime control:
|
|
104
|
+
|
|
105
|
+
- Read the exact thread before every send.
|
|
106
|
+
- Never infer a recipient from a partial name.
|
|
107
|
+
- Never send a duplicate or retry an ambiguous timeout without re-reading.
|
|
108
|
+
- Do not parallelize sends or work around a rate limit.
|
|
109
|
+
- Stop on a provider warning, checkpoint, restriction, HTTP 403, or HTTP 429.
|
|
110
|
+
- Avoid bulk profile reads, copied message bursts, cold-account automation,
|
|
111
|
+
and unsolicited WhatsApp messaging.
|
|
112
|
+
|
|
113
|
+
SignalDash exposes no LinkedIn invitation tool.
|
|
113
114
|
|
|
114
115
|
## Development
|
|
115
116
|
|
package/bin/sd.mjs
CHANGED
|
@@ -3,48 +3,109 @@
|
|
|
3
3
|
// bearer token. Never holds a Unipile key, never calls Unipile directly. The MCP
|
|
4
4
|
// tools an agent uses all proxy through the backend, so agents access channels
|
|
5
5
|
// THROUGH SignalDash, not around it.
|
|
6
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { readFileSync, realpathSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
|
-
const CFG_DIR = process.env.SIGNALDASH_HOME || `${homedir()}/.signaldash`;
|
|
11
|
-
const CFG = `${CFG_DIR}/config.json`;
|
|
12
11
|
const DEFAULT_BACKEND = process.env.SIGNALDASH_BACKEND || "https://signaldash-api.floom.dev";
|
|
13
12
|
|
|
14
|
-
function
|
|
15
|
-
|
|
13
|
+
function configPaths() {
|
|
14
|
+
const directory = process.env.SIGNALDASH_HOME || `${homedir()}/.signaldash`;
|
|
15
|
+
return { directory, file: `${directory}/config.json` };
|
|
16
|
+
}
|
|
17
|
+
function loadCfg() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(configPaths().file, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveCfg(c) {
|
|
25
|
+
const config = configPaths();
|
|
26
|
+
mkdirSync(config.directory, { recursive: true });
|
|
27
|
+
writeFileSync(config.file, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
16
29
|
|
|
17
|
-
async function api(path, body, { auth = true } = {}) {
|
|
30
|
+
async function api(path, body, { auth = true, method = "POST" } = {}) {
|
|
18
31
|
const cfg = loadCfg();
|
|
19
32
|
const backend = cfg.backend || DEFAULT_BACKEND;
|
|
20
33
|
const r = await fetch(backend + path, {
|
|
21
|
-
method
|
|
34
|
+
method,
|
|
22
35
|
headers: { "Content-Type": "application/json", ...(auth && cfg.token ? { Authorization: `Bearer ${cfg.token}` } : {}) },
|
|
23
|
-
body: JSON.stringify(body || {}),
|
|
36
|
+
...(method === "GET" ? {} : { body: JSON.stringify(body || {}) }),
|
|
24
37
|
});
|
|
25
38
|
return { status: r.status, json: await r.json().catch(() => ({})) };
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
42
|
+
|
|
43
|
+
function positiveMilliseconds(name, fallback) {
|
|
44
|
+
const raw = process.env[name];
|
|
45
|
+
if (raw === undefined) return fallback;
|
|
46
|
+
const parsed = Number(raw);
|
|
47
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function cmdLogin(code, backend, dependencies = {}) {
|
|
51
|
+
const request = dependencies.request || api;
|
|
52
|
+
const log = dependencies.log || console.log;
|
|
53
|
+
const error = dependencies.error || console.error;
|
|
29
54
|
const cfg = loadCfg();
|
|
30
55
|
cfg.backend = backend || cfg.backend || DEFAULT_BACKEND;
|
|
31
56
|
saveCfg(cfg);
|
|
32
|
-
const r = await
|
|
33
|
-
if (r.status !== 200) {
|
|
57
|
+
const r = await request("/login", { code }, { auth: false });
|
|
58
|
+
if (r.status !== 200) { error("login failed:", r.json.error || r.status); process.exitCode = 1; return; }
|
|
34
59
|
cfg.token = r.json.token; saveCfg(cfg);
|
|
35
|
-
|
|
60
|
+
log("Logged in to SignalDash. Token stored (no channel keys on your side).");
|
|
36
61
|
}
|
|
37
62
|
|
|
38
|
-
async function cmdConnect(provider) {
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
export async function cmdConnect(provider, dependencies = {}) {
|
|
64
|
+
const request = dependencies.request || api;
|
|
65
|
+
const sleep = dependencies.wait || wait;
|
|
66
|
+
const now = dependencies.now || Date.now;
|
|
67
|
+
const log = dependencies.log || console.log;
|
|
68
|
+
const error = dependencies.error || console.error;
|
|
69
|
+
if (!["linkedin", "whatsapp"].includes(provider)) {
|
|
70
|
+
error("usage: signaldash connect linkedin|whatsapp");
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const r = await request(`/connect/${provider}`, {});
|
|
75
|
+
if (r.status >= 300) {
|
|
76
|
+
error("connect failed:", r.json.error || r.status);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
log(`Open this to connect your ${provider}:\n\n ${r.json.url}\n`);
|
|
81
|
+
log("Waiting for authentication to complete...");
|
|
82
|
+
|
|
83
|
+
const timeoutMs = positiveMilliseconds("SIGNALDASH_CONNECT_TIMEOUT_MS", 5 * 60 * 1000);
|
|
84
|
+
const pollMs = positiveMilliseconds("SIGNALDASH_CONNECT_POLL_MS", 2000);
|
|
85
|
+
const deadline = now() + timeoutMs;
|
|
86
|
+
while (now() < deadline) {
|
|
87
|
+
const status = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
|
|
88
|
+
if (status.status === 200 && status.json.connected) {
|
|
89
|
+
log(`Connected ${provider}${status.json.name ? `: ${status.json.name}` : ""}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (status.status !== 202) {
|
|
93
|
+
error("connect status failed:", status.json.error || status.status);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - now())));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
error(`Connection timed out. Manual fallback:\n signaldash connect ${provider} claim <account_id>`);
|
|
101
|
+
process.exitCode = 1;
|
|
44
102
|
}
|
|
45
|
-
async function cmdClaim(provider, accountId) {
|
|
46
|
-
const
|
|
47
|
-
|
|
103
|
+
export async function cmdClaim(provider, accountId, dependencies = {}) {
|
|
104
|
+
const request = dependencies.request || api;
|
|
105
|
+
const log = dependencies.log || console.log;
|
|
106
|
+
const r = await request(`/connect/${provider}/claim`, { account_id: accountId });
|
|
107
|
+
log(r.status === 200 ? `Connected ${provider}: ${r.json.name}` : `claim failed: ${r.json.error}`);
|
|
108
|
+
if (r.status !== 200) process.exitCode = 1;
|
|
48
109
|
}
|
|
49
110
|
|
|
50
111
|
// ---- MCP (stdio). Every tool proxies through the backend with the token. ----
|
|
@@ -60,9 +121,12 @@ function mcpTool(name) {
|
|
|
60
121
|
return { name, description: TOOLS.find(t => t.name === name).description,
|
|
61
122
|
inputSchema: { type: "object", properties: { chat_id: { type: "string" }, text: { type: "string" }, limit: { type: "number" } } } };
|
|
62
123
|
}
|
|
63
|
-
async function runMcp() {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
124
|
+
export async function runMcp(dependencies = {}) {
|
|
125
|
+
const input = dependencies.input || process.stdin;
|
|
126
|
+
const output = dependencies.output || process.stdout;
|
|
127
|
+
const request = dependencies.request || api;
|
|
128
|
+
const rl = createInterface({ input });
|
|
129
|
+
const reply = (id, result, error) => output.write(JSON.stringify({ jsonrpc: "2.0", id, ...(error ? { error } : { result }) }) + "\n");
|
|
66
130
|
for await (const line of rl) {
|
|
67
131
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
68
132
|
const { id, method, params } = msg;
|
|
@@ -71,15 +135,41 @@ async function runMcp() {
|
|
|
71
135
|
else if (method === "tools/call") {
|
|
72
136
|
const t = TOOLS.find(x => x.name === params.name);
|
|
73
137
|
if (!t) { reply(id, null, { code: -32601, message: "unknown tool" }); continue; }
|
|
74
|
-
const r = await
|
|
138
|
+
const r = await request(`/${t.ch}/${t.action}`, params.arguments || {});
|
|
75
139
|
reply(id, { content: [{ type: "text", text: JSON.stringify(r.json) }], isError: r.status >= 300 });
|
|
76
140
|
} else if (id !== undefined) reply(id, {});
|
|
77
141
|
}
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
|
|
145
|
+
export async function cmdSkill(dependencies = {}) {
|
|
146
|
+
const log = dependencies.log || console.log;
|
|
147
|
+
const { mkdirSync: mk, writeFileSync: wf, readFileSync: rf, existsSync: ex } = await import("node:fs");
|
|
148
|
+
const { dirname, join } = await import("node:path");
|
|
149
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
150
|
+
const src = join(here, "..", "skills", "signaldash", "SKILL.md");
|
|
151
|
+
if (!ex(src)) { log("skill file not found in package"); process.exitCode = 1; return; }
|
|
152
|
+
const dest = join(homedir(), ".claude", "skills", "signaldash");
|
|
153
|
+
mk(dest, { recursive: true });
|
|
154
|
+
wf(join(dest, "SKILL.md"), rf(src, "utf8"));
|
|
155
|
+
log(`Installed the SignalDash skill to ${dest}/SKILL.md`);
|
|
156
|
+
log("Your agent now knows how to use LinkedIn + WhatsApp safely through SignalDash.");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
160
|
+
const [cmd, a, b, c] = argv;
|
|
161
|
+
const log = dependencies.log || console.log;
|
|
162
|
+
if (cmd === "login") await cmdLogin(a, b === "--backend" ? c : undefined, dependencies);
|
|
163
|
+
else if (cmd === "connect" && b === "claim") await cmdClaim(a, c, dependencies);
|
|
164
|
+
else if (cmd === "connect") await cmdConnect(a, dependencies);
|
|
165
|
+
else if (cmd === "mcp") await runMcp(dependencies);
|
|
166
|
+
else if (cmd === "skill") await cmdSkill(dependencies);
|
|
167
|
+
else log(`SignalDash — secure LinkedIn + WhatsApp access for your agent.\n\n signaldash login <invite-code> [--backend URL]\n signaldash connect linkedin|whatsapp\n signaldash connect linkedin|whatsapp claim <account_id>\n signaldash mcp\n signaldash skill install the agent skill\n\nThe agent reaches channels only through SignalDash. No keys on your machine.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
process.argv[1] &&
|
|
172
|
+
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
173
|
+
) {
|
|
174
|
+
await main();
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/signaldash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Secure LinkedIn and WhatsApp MCP access for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node --test",
|
|
21
|
-
"check": "node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
|
|
21
|
+
"check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"mcp",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signaldash
|
|
3
|
+
description: Give this agent secure access to the user's LinkedIn and WhatsApp — list chats, read threads, and send messages — through SignalDash. Use whenever the user asks to check, read, triage, reply to, or send LinkedIn/WhatsApp messages. Also covers first-time setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SignalDash — LinkedIn + WhatsApp for this agent
|
|
7
|
+
|
|
8
|
+
SignalDash is the gate between you and the user's real messaging accounts. The
|
|
9
|
+
user's channel credentials live on the SignalDash server, never on their machine
|
|
10
|
+
and never in your context. You act through SignalDash tools only.
|
|
11
|
+
|
|
12
|
+
## Setup (only if the tools are not available yet)
|
|
13
|
+
|
|
14
|
+
The user needs an invite code from Federico, then:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @floomhq/signaldash login <invite-code>
|
|
18
|
+
npx @floomhq/signaldash connect linkedin # opens a link; user authenticates; auto-detects
|
|
19
|
+
npx @floomhq/signaldash connect whatsapp # same, QR scan from WhatsApp > Linked Devices
|
|
20
|
+
claude mcp add signaldash -- npx -y @floomhq/signaldash mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Cursor: add to `.cursor/mcp.json` →
|
|
24
|
+
`{"mcpServers":{"signaldash":{"command":"npx","args":["-y","@floomhq/signaldash","mcp"]}}}`
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | What it does |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `li_list_chats` | list the user's LinkedIn chats |
|
|
31
|
+
| `li_read_messages` | read a LinkedIn thread (`chat_id`) |
|
|
32
|
+
| `li_send_message` | send a LinkedIn message (`chat_id`, `text`) |
|
|
33
|
+
| `wa_list_chats` | list the user's WhatsApp chats |
|
|
34
|
+
| `wa_read_messages` | read a WhatsApp thread (`chat_id`) |
|
|
35
|
+
| `wa_send_message` | send a WhatsApp message (`chat_id`, `text`) |
|
|
36
|
+
|
|
37
|
+
## The rules you MUST follow
|
|
38
|
+
|
|
39
|
+
These protect the user's accounts from being restricted or banned, and protect
|
|
40
|
+
them from you sending something wrong. They are not optional.
|
|
41
|
+
|
|
42
|
+
1. **Never send without the user's explicit approval of the exact recipient and
|
|
43
|
+
exact text.** Draft it, show it, wait. Discussion is not approval.
|
|
44
|
+
2. **Read the thread before you send.** Always `*_read_messages` on that exact
|
|
45
|
+
chat first: check who it is, what they last said, and whether this was
|
|
46
|
+
already sent. Never send blind. Never double-send.
|
|
47
|
+
3. **Never bulk.** No mass messaging, no "message everyone who…", no bulk
|
|
48
|
+
profile fetching. LinkedIn restricts accounts for exactly this. Hard ceiling
|
|
49
|
+
is roughly 15-20 LinkedIn sends per day; the server enforces a daily cap and
|
|
50
|
+
spacing, do not try to work around it or parallelize sends.
|
|
51
|
+
4. **One message at a time, human pace.** Let the server's pacing do its work.
|
|
52
|
+
5. **Stop on any warning.** A checkpoint, restriction, "unusual activity"
|
|
53
|
+
prompt, 403 or 429 means halt all activity on that account and tell the user.
|
|
54
|
+
Do not retry.
|
|
55
|
+
6. **Personalize.** Read the thread and write a specific reply. Templated
|
|
56
|
+
identical messages get reported as spam.
|
|
57
|
+
|
|
58
|
+
## Good vs bad requests
|
|
59
|
+
|
|
60
|
+
Good: "list my unread LinkedIn chats and draft replies for my approval" ·
|
|
61
|
+
"read the thread with Mircea and suggest an answer" · "what came in on WhatsApp
|
|
62
|
+
from people I've spoken to before?"
|
|
63
|
+
|
|
64
|
+
Bad (refuse or push back): "connection-request everyone who liked my post" ·
|
|
65
|
+
"message all my connections about X" · "pull every profile in my network".
|
|
66
|
+
|
|
67
|
+
Rule of thumb: if a human could not plausibly do it by hand in a day, do not do
|
|
68
|
+
it. Account health beats throughput.
|