whats-mcp 0.1.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/.dockerignore +12 -0
- package/.gitlab-ci.yml +54 -0
- package/CHANGELOG.md +38 -0
- package/README.md +205 -0
- package/TODO.md +6 -0
- package/config.json +19 -0
- package/package.json +46 -0
- package/src/.env.example +21 -0
- package/src/admin/cli.js +916 -0
- package/src/admin/service.js +271 -0
- package/src/admin/telegram.js +178 -0
- package/src/admin.js +12 -0
- package/src/config.js +147 -0
- package/src/connection.js +334 -0
- package/src/helpers.js +264 -0
- package/src/http_app.js +267 -0
- package/src/index.js +4 -0
- package/src/main.js +71 -0
- package/src/server.js +67 -0
- package/src/store.js +925 -0
- package/src/tools/analytics.js +157 -0
- package/src/tools/channels.js +215 -0
- package/src/tools/chats.js +291 -0
- package/src/tools/contacts.js +259 -0
- package/src/tools/digest.js +249 -0
- package/src/tools/groups.js +529 -0
- package/src/tools/history-support.js +114 -0
- package/src/tools/labels.js +168 -0
- package/src/tools/messaging.js +510 -0
- package/src/tools/overview.js +416 -0
- package/src/tools/profile.js +155 -0
- package/src/tools/registry.js +105 -0
- package/src/tools/tags.js +104 -0
- package/src/tools/utils.js +325 -0
- package/src/tools/watchlists.js +136 -0
package/src/http_app.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — HTTP surface.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const express = require("express");
|
|
9
|
+
const { StreamableHTTPServerTransport } = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
10
|
+
|
|
11
|
+
const { connect, getConnectionInfo } = require("./connection");
|
|
12
|
+
const { loadConfig } = require("./config");
|
|
13
|
+
const { createLogger, createMcpServer } = require("./server");
|
|
14
|
+
const {
|
|
15
|
+
adminHelpText,
|
|
16
|
+
authSummary,
|
|
17
|
+
healthSummaryText,
|
|
18
|
+
appendAdminLog,
|
|
19
|
+
pairingRuntimeStatus,
|
|
20
|
+
requestPairingCode,
|
|
21
|
+
statusSummaryText,
|
|
22
|
+
urlsSummary,
|
|
23
|
+
} = require("./admin/service");
|
|
24
|
+
const { requestReconnect } = require("./connection");
|
|
25
|
+
const {
|
|
26
|
+
startTelegramAdmin,
|
|
27
|
+
telegramAdminEnabled,
|
|
28
|
+
telegramAdminRuntimeStatus,
|
|
29
|
+
} = require("./admin/telegram");
|
|
30
|
+
|
|
31
|
+
function basePayload(config) {
|
|
32
|
+
const summary = authSummary();
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
product: "whats-mcp",
|
|
36
|
+
service: "WhatsApp MCP transport bridge",
|
|
37
|
+
version: config.server.version,
|
|
38
|
+
transport: "streamable-http",
|
|
39
|
+
mcp_path: config.server.http_mcp_path,
|
|
40
|
+
public_base_url: config.server.public_base_url,
|
|
41
|
+
fallback_base_url: config.server.fallback_base_url,
|
|
42
|
+
listen_port: config.server.http_port,
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
running: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function healthHandler(config) {
|
|
49
|
+
return async (_req, res) => {
|
|
50
|
+
const payload = basePayload(config);
|
|
51
|
+
payload.auth = {
|
|
52
|
+
state_directory: authSummary().state_directory,
|
|
53
|
+
auth_directory: authSummary().auth_directory,
|
|
54
|
+
auth_persisted: authSummary().auth_present,
|
|
55
|
+
connection_state: getConnectionInfo().state,
|
|
56
|
+
};
|
|
57
|
+
res.json(payload);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function adminStatusHandler(config) {
|
|
62
|
+
return async (_req, res) => {
|
|
63
|
+
const payload = basePayload(config);
|
|
64
|
+
payload.admin = {
|
|
65
|
+
ssh_admin: {
|
|
66
|
+
supported: true,
|
|
67
|
+
examples: [
|
|
68
|
+
"docker compose exec -T whats-mcp whats-admin status",
|
|
69
|
+
"docker compose logs --tail=100 whats-mcp",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
telegram_admin: {
|
|
73
|
+
supported: true,
|
|
74
|
+
token_env: "TELEGRAM_WHATS_HOMELAB_TOKEN",
|
|
75
|
+
allowed_chat_ids_env: "TELEGRAM_CHAT_IDS",
|
|
76
|
+
configured: telegramAdminEnabled(),
|
|
77
|
+
enabled: telegramAdminEnabled(),
|
|
78
|
+
runtime: telegramAdminRuntimeStatus(),
|
|
79
|
+
},
|
|
80
|
+
auth_probe: {
|
|
81
|
+
state_directory: authSummary().state_directory,
|
|
82
|
+
auth_directory: authSummary().auth_directory,
|
|
83
|
+
auth_persisted: authSummary().auth_present,
|
|
84
|
+
connection_state: getConnectionInfo().state,
|
|
85
|
+
},
|
|
86
|
+
pairing_runtime: pairingRuntimeStatus(),
|
|
87
|
+
status_summary: statusSummaryText({
|
|
88
|
+
pid: process.pid,
|
|
89
|
+
running: true,
|
|
90
|
+
connection_state: getConnectionInfo().state,
|
|
91
|
+
user: getConnectionInfo().user,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
payload.routes = {
|
|
95
|
+
health: "/health",
|
|
96
|
+
admin_status: "/admin/status",
|
|
97
|
+
admin_help: "/admin/help",
|
|
98
|
+
admin_reconnect: "/admin/reconnect",
|
|
99
|
+
admin_pair_code: "/admin/pair-code",
|
|
100
|
+
mcp: config.server.http_mcp_path,
|
|
101
|
+
};
|
|
102
|
+
res.json(payload);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function adminHelpHandler(config) {
|
|
107
|
+
return async (_req, res) => {
|
|
108
|
+
const payload = basePayload(config);
|
|
109
|
+
payload.help = {
|
|
110
|
+
text: adminHelpText(),
|
|
111
|
+
summaries: {
|
|
112
|
+
status: statusSummaryText(),
|
|
113
|
+
health: healthSummaryText(),
|
|
114
|
+
urls: urlsSummary(),
|
|
115
|
+
},
|
|
116
|
+
routes: {
|
|
117
|
+
health: "/health",
|
|
118
|
+
admin_status: "/admin/status",
|
|
119
|
+
admin_help: "/admin/help",
|
|
120
|
+
admin_reconnect: "/admin/reconnect",
|
|
121
|
+
admin_pair_code: "/admin/pair-code",
|
|
122
|
+
mcp: config.server.http_mcp_path,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
res.json(payload);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function adminReconnectHandler(handlers = {}) {
|
|
130
|
+
return async (_req, res) => {
|
|
131
|
+
appendAdminLog("http admin reconnect requested");
|
|
132
|
+
try {
|
|
133
|
+
if (handlers.onReconnect) {
|
|
134
|
+
await handlers.onReconnect();
|
|
135
|
+
} else {
|
|
136
|
+
await requestReconnect();
|
|
137
|
+
}
|
|
138
|
+
res.json({
|
|
139
|
+
ok: true,
|
|
140
|
+
action: "reconnect",
|
|
141
|
+
message: "whats-mcp reconnect requested",
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
res.status(500).json({
|
|
145
|
+
ok: false,
|
|
146
|
+
action: "reconnect",
|
|
147
|
+
error: error.message || String(error),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function adminPairCodeHandler() {
|
|
154
|
+
return async (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const pairing = await requestPairingCode(req.body?.phone || "");
|
|
157
|
+
appendAdminLog(`http admin pairing code generated for ${pairing.phone}`);
|
|
158
|
+
res.json({
|
|
159
|
+
ok: true,
|
|
160
|
+
action: "pair-code",
|
|
161
|
+
phone: pairing.phone,
|
|
162
|
+
code: pairing.code,
|
|
163
|
+
message: "Pairing code generated. Enter it on your phone before it expires.",
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
res.status(400).json({
|
|
167
|
+
ok: false,
|
|
168
|
+
action: "pair-code",
|
|
169
|
+
error: error.message || String(error),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function createHttpApp(handlers = {}) {
|
|
176
|
+
const config = loadConfig();
|
|
177
|
+
const logger = createLogger(config);
|
|
178
|
+
const app = express();
|
|
179
|
+
const transports = new Map();
|
|
180
|
+
|
|
181
|
+
app.use(express.json({ limit: "1mb" }));
|
|
182
|
+
|
|
183
|
+
app.get("/health", healthHandler(config));
|
|
184
|
+
app.get("/admin/status", adminStatusHandler(config));
|
|
185
|
+
app.get("/admin/help", adminHelpHandler(config));
|
|
186
|
+
app.post("/admin/reconnect", adminReconnectHandler(handlers));
|
|
187
|
+
app.post("/admin/pair-code", adminPairCodeHandler());
|
|
188
|
+
|
|
189
|
+
const mcpPostHandler = async (req, res) => {
|
|
190
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
191
|
+
try {
|
|
192
|
+
let transport;
|
|
193
|
+
if (sessionId && transports.has(sessionId)) {
|
|
194
|
+
transport = transports.get(sessionId);
|
|
195
|
+
} else if (!sessionId && req.body && req.body.method === "initialize") {
|
|
196
|
+
transport = new StreamableHTTPServerTransport({
|
|
197
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
198
|
+
onsessioninitialized: (newSessionId) => {
|
|
199
|
+
transports.set(newSessionId, transport);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
transport.onclose = () => {
|
|
203
|
+
if (transport.sessionId) {
|
|
204
|
+
transports.delete(transport.sessionId);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const server = createMcpServer(config, logger);
|
|
208
|
+
await server.connect(transport);
|
|
209
|
+
await transport.handleRequest(req, res, req.body);
|
|
210
|
+
return;
|
|
211
|
+
} else {
|
|
212
|
+
res.status(400).json({
|
|
213
|
+
jsonrpc: "2.0",
|
|
214
|
+
error: { code: -32000, message: "Bad Request: No valid session ID provided" },
|
|
215
|
+
id: null,
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await transport.handleRequest(req, res, req.body);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logger.error({ err: error }, "Error handling HTTP MCP POST request");
|
|
222
|
+
if (!res.headersSent) {
|
|
223
|
+
res.status(500).json({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
error: { code: -32603, message: "Internal server error" },
|
|
226
|
+
id: null,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
app.post(config.server.http_mcp_path, mcpPostHandler);
|
|
233
|
+
app.get(config.server.http_mcp_path, async (req, res) => {
|
|
234
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
235
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
236
|
+
res.status(400).send("Invalid or missing session ID");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await transports.get(sessionId).handleRequest(req, res);
|
|
240
|
+
});
|
|
241
|
+
app.delete(config.server.http_mcp_path, async (req, res) => {
|
|
242
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
243
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
244
|
+
res.status(400).send("Invalid or missing session ID");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await transports.get(sessionId).handleRequest(req, res);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return { app, config };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function bootstrapHttpRuntime(handlers = {}) {
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
connect(config).catch(() => {});
|
|
256
|
+
startTelegramAdmin(handlers);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
adminHelpHandler,
|
|
261
|
+
adminPairCodeHandler,
|
|
262
|
+
adminReconnectHandler,
|
|
263
|
+
adminStatusHandler,
|
|
264
|
+
bootstrapHttpRuntime,
|
|
265
|
+
createHttpApp,
|
|
266
|
+
healthHandler,
|
|
267
|
+
};
|
package/src/index.js
ADDED
package/src/main.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* whats-mcp — transport entrypoint.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
|
|
10
|
+
const { connect, requestReconnect } = require("./connection");
|
|
11
|
+
const { loadConfig } = require("./config");
|
|
12
|
+
const { createHttpApp, bootstrapHttpRuntime } = require("./http_app");
|
|
13
|
+
const { createLogger, createMcpServer } = require("./server");
|
|
14
|
+
|
|
15
|
+
async function serveStdio() {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const logger = createLogger(config);
|
|
18
|
+
const server = createMcpServer(config, logger);
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
logger.info("MCP server connected via stdio");
|
|
22
|
+
connect(config).catch((err) => {
|
|
23
|
+
logger.error({ err }, "WhatsApp connection failed");
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function serveHttp() {
|
|
28
|
+
const handlers = {
|
|
29
|
+
onReconnect: () => requestReconnect(),
|
|
30
|
+
onRestart: () => process.exit(0),
|
|
31
|
+
};
|
|
32
|
+
const { app, config } = await createHttpApp(handlers);
|
|
33
|
+
const logger = createLogger(config);
|
|
34
|
+
await bootstrapHttpRuntime(handlers);
|
|
35
|
+
app.listen(config.server.http_port, config.server.http_host, () => {
|
|
36
|
+
logger.info(
|
|
37
|
+
{
|
|
38
|
+
host: config.server.http_host,
|
|
39
|
+
port: config.server.http_port,
|
|
40
|
+
mcpPath: config.server.http_mcp_path,
|
|
41
|
+
},
|
|
42
|
+
"whats-mcp HTTP transport listening",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(argv = process.argv.slice(2)) {
|
|
48
|
+
const command = argv[0] || "serve";
|
|
49
|
+
if (command === "serve" || command === "stdio") {
|
|
50
|
+
await serveStdio();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (command === "serve-http") {
|
|
54
|
+
await serveHttp();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Unknown command: ${command}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (require.main === module) {
|
|
61
|
+
main().catch((err) => {
|
|
62
|
+
console.error("Fatal error:", err);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
main,
|
|
69
|
+
serveHttp,
|
|
70
|
+
serveStdio,
|
|
71
|
+
};
|
package/src/server.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — shared MCP server construction.
|
|
3
|
+
*
|
|
4
|
+
* The same logical MCP surface is reused by:
|
|
5
|
+
* - stdio transport
|
|
6
|
+
* - HTTP streamable transport
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
12
|
+
const {
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
16
|
+
const pino = require("pino");
|
|
17
|
+
|
|
18
|
+
const { getSocket, getStore, getConnectionInfo } = require("./connection");
|
|
19
|
+
const { listTools, callTool } = require("./tools/registry");
|
|
20
|
+
|
|
21
|
+
function createLogger(config) {
|
|
22
|
+
return pino({ level: config.logging?.level || "error" }, pino.destination(2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createMcpServer(config, logger = createLogger(config)) {
|
|
26
|
+
const server = new Server(
|
|
27
|
+
{
|
|
28
|
+
name: config.server.name,
|
|
29
|
+
version: config.server.version,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
capabilities: {
|
|
33
|
+
tools: {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
39
|
+
return { tools: listTools() };
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
43
|
+
const { name, arguments: args } = request.params;
|
|
44
|
+
const ctx = {
|
|
45
|
+
get sock() {
|
|
46
|
+
return getSocket();
|
|
47
|
+
},
|
|
48
|
+
store: getStore(),
|
|
49
|
+
connectionInfo: getConnectionInfo,
|
|
50
|
+
config,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
logger.info({ tool: name }, "CallTool");
|
|
54
|
+
const result = await callTool(name, args, ctx);
|
|
55
|
+
if (result.isError) {
|
|
56
|
+
logger.warn({ tool: name, result }, "Tool error");
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return server;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
createLogger,
|
|
66
|
+
createMcpServer,
|
|
67
|
+
};
|