signal-db-cli 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/README.md +248 -0
- package/docs/MANUAL.md +187 -0
- package/lib/signal-db.js +432 -0
- package/package.json +47 -0
- package/signal-db-cli.js +625 -0
- package/signal-db-mcp.js +206 -0
package/signal-db-mcp.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP (Model Context Protocol) server for browsing a local Signal Desktop database.
|
|
5
|
+
*
|
|
6
|
+
* Exposes the same read-only query functionality as the CLI via stdio transport.
|
|
7
|
+
* Tools: get_messages, get_conversations, get_calls, get_message_by_id, get_phone.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: No console.log — stdout is the JSON-RPC channel.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import pkg from './package.json' with { type: 'json' };
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
openDB,
|
|
22
|
+
formatDate,
|
|
23
|
+
getMessages,
|
|
24
|
+
getConversations,
|
|
25
|
+
findConversations,
|
|
26
|
+
getCalls,
|
|
27
|
+
getMessageById,
|
|
28
|
+
} from './lib/signal-db.js';
|
|
29
|
+
|
|
30
|
+
// Load env (same locations as CLI)
|
|
31
|
+
dotenv.config({ quiet: true });
|
|
32
|
+
dotenv.config({ path: path.join(os.homedir(), '.signal-db-cli', '.env'), quiet: true });
|
|
33
|
+
|
|
34
|
+
// Validate key
|
|
35
|
+
if (!process.env.SIGNAL_DECRYPTION_KEY) {
|
|
36
|
+
console.error('SIGNAL_DECRYPTION_KEY is not set. Set it in .env or ~/.signal-db-cli/.env');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Open DB once (read-only, long-lived process)
|
|
41
|
+
let db;
|
|
42
|
+
try {
|
|
43
|
+
db = openDB();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`Failed to open Signal database: ${err.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const server = new McpServer({
|
|
50
|
+
name: 'signal-db',
|
|
51
|
+
version: pkg.version,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// --- Tool: get_messages ---
|
|
55
|
+
server.tool(
|
|
56
|
+
'get_messages',
|
|
57
|
+
'Search and filter Signal messages. Supports full-text search, conversation filter, unread/unanswered filters, date ranges (ISO or relative like 5h/3d), and direction filters.',
|
|
58
|
+
{
|
|
59
|
+
search: z.string().optional().describe('Full-text search query (spaces=OR, commas=AND, prefix matching)'),
|
|
60
|
+
conv: z.string().optional().describe('Conversation filter: name (searches all matches), =exact name, or UUID'),
|
|
61
|
+
unread: z.boolean().optional().describe('Only unread incoming messages'),
|
|
62
|
+
unanswered: z.boolean().optional().describe('Only unanswered incoming messages'),
|
|
63
|
+
olderThan: z.number().optional().describe('Hours threshold for unanswered filter (default 24)'),
|
|
64
|
+
from: z.string().optional().describe('Start date (ISO like 2025-01-15, or relative like 5h/3d/10m)'),
|
|
65
|
+
to: z.string().optional().describe('End date (ISO or relative)'),
|
|
66
|
+
incoming: z.boolean().optional().describe('Only incoming messages'),
|
|
67
|
+
outgoing: z.boolean().optional().describe('Only outgoing messages'),
|
|
68
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
69
|
+
},
|
|
70
|
+
async (params) => {
|
|
71
|
+
try {
|
|
72
|
+
const result = getMessages(db, {
|
|
73
|
+
search: params.search,
|
|
74
|
+
conv: params.conv,
|
|
75
|
+
unread: params.unread ?? false,
|
|
76
|
+
unanswered: params.unanswered ?? false,
|
|
77
|
+
olderThan: params.olderThan ?? 24,
|
|
78
|
+
from: params.from,
|
|
79
|
+
to: params.to,
|
|
80
|
+
incoming: params.incoming ?? false,
|
|
81
|
+
outgoing: params.outgoing ?? false,
|
|
82
|
+
limit: params.limit ?? 20,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const messages = result.messages.map((m) => ({
|
|
86
|
+
...m,
|
|
87
|
+
date: formatDate(m.sent_at),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [{
|
|
92
|
+
type: 'text',
|
|
93
|
+
text: JSON.stringify({ messages, total: result.total, conversationName: result.conversationName }, null, 2),
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// --- Tool: get_conversations ---
|
|
103
|
+
server.tool(
|
|
104
|
+
'get_conversations',
|
|
105
|
+
'List or search Signal conversations. Without a query, lists recent conversations. With a query, searches by name/phone/ID.',
|
|
106
|
+
{
|
|
107
|
+
query: z.string().optional().describe('Search conversations by name, phone number, or ID'),
|
|
108
|
+
type: z.enum(['private', 'group']).optional().describe('Filter by conversation type'),
|
|
109
|
+
limit: z.number().optional().describe('Max results (default 50)'),
|
|
110
|
+
},
|
|
111
|
+
async (params) => {
|
|
112
|
+
let convs;
|
|
113
|
+
if (params.query) {
|
|
114
|
+
convs = findConversations(db, params.query, {
|
|
115
|
+
type: params.type ?? null,
|
|
116
|
+
limit: params.limit ?? 20,
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
convs = getConversations(db, {
|
|
120
|
+
type: params.type ?? null,
|
|
121
|
+
limit: params.limit ?? 50,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const enriched = convs.map((c) => ({
|
|
126
|
+
...c,
|
|
127
|
+
lastActive: formatDate(c.active_at),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify({ conversations: enriched }, null, 2),
|
|
134
|
+
}],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// --- Tool: get_calls ---
|
|
140
|
+
server.tool(
|
|
141
|
+
'get_calls',
|
|
142
|
+
'Get recent Signal call history.',
|
|
143
|
+
{
|
|
144
|
+
limit: z.number().optional().describe('Max results (default 20)'),
|
|
145
|
+
},
|
|
146
|
+
async (params) => {
|
|
147
|
+
const calls = getCalls(db, params.limit ?? 20);
|
|
148
|
+
const enriched = calls.map((c) => ({
|
|
149
|
+
...c,
|
|
150
|
+
date: formatDate(c.timestamp),
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [{
|
|
155
|
+
type: 'text',
|
|
156
|
+
text: JSON.stringify({ calls: enriched }, null, 2),
|
|
157
|
+
}],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// --- Tool: get_message_by_id ---
|
|
163
|
+
server.tool(
|
|
164
|
+
'get_message_by_id',
|
|
165
|
+
'Retrieve a single Signal message by its ID, including full body text.',
|
|
166
|
+
{
|
|
167
|
+
id: z.string().describe('Message ID'),
|
|
168
|
+
},
|
|
169
|
+
async (params) => {
|
|
170
|
+
const msg = getMessageById(db, params.id);
|
|
171
|
+
if (!msg) {
|
|
172
|
+
return { content: [{ type: 'text', text: 'Message not found' }], isError: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: JSON.stringify({ ...msg, date: formatDate(msg.sent_at) }, null, 2),
|
|
179
|
+
}],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// --- Tool: get_phone ---
|
|
185
|
+
server.tool(
|
|
186
|
+
'get_phone',
|
|
187
|
+
'Look up phone numbers by contact name.',
|
|
188
|
+
{
|
|
189
|
+
query: z.string().describe('Contact name to search for'),
|
|
190
|
+
},
|
|
191
|
+
async (params) => {
|
|
192
|
+
const convs = findConversations(db, params.query).filter((c) => c.e164);
|
|
193
|
+
const contacts = convs.map((c) => ({ name: c.name, phone: c.e164 }));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
content: [{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: JSON.stringify({ contacts }, null, 2),
|
|
199
|
+
}],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Connect via stdio
|
|
205
|
+
const transport = new StdioServerTransport();
|
|
206
|
+
await server.connect(transport);
|