@thxmxx/telegram-mcp 1.2.0 → 1.2.1
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/package.json +1 -1
- package/src/index.js +129 -23
- package/src/setup.js +11 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* telegram_notify — send a message (fire and forget)
|
|
7
7
|
* telegram_ask — ask a question, wait for text reply
|
|
8
8
|
* telegram_choose — show buttons, wait for a tap
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* No per-project configuration needed.
|
|
9
|
+
* telegram_listen — wait for user to address this instance by name,
|
|
10
|
+
* returns the next instruction so Claude can keep working
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -36,35 +35,36 @@ if (existsSync(envPath)) {
|
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
const TOKEN
|
|
38
|
+
const TOKEN = env.TELEGRAM_BOT_TOKEN;
|
|
40
39
|
const CHAT_ID = env.TELEGRAM_CHAT_ID;
|
|
41
40
|
|
|
42
41
|
if (!TOKEN || !CHAT_ID) {
|
|
43
42
|
process.stderr.write(
|
|
44
43
|
"[telegram-mcp] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID.\n" +
|
|
45
|
-
|
|
44
|
+
" Run: npx @thxmxx/telegram-mcp init\n",
|
|
46
45
|
);
|
|
47
46
|
exit(1);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
// ── Auto instance label: folder#shortid ───────────────────────────────────────
|
|
51
50
|
|
|
52
|
-
const folder
|
|
53
|
-
const shortId
|
|
54
|
-
const INSTANCE
|
|
55
|
-
const HDR
|
|
51
|
+
const folder = process.cwd().split("/").pop() || "claude";
|
|
52
|
+
const shortId = randomBytes(2).toString("hex");
|
|
53
|
+
const INSTANCE = `${folder}#${shortId}`;
|
|
54
|
+
const HDR = `\`[${INSTANCE}]\``;
|
|
56
55
|
|
|
57
56
|
// ── Telegram client ───────────────────────────────────────────────────────────
|
|
58
57
|
|
|
59
58
|
const bot = new TelegramBot(TOKEN, { polling: true });
|
|
60
59
|
|
|
60
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
61
62
|
function waitForReply(timeoutMs = 300_000) {
|
|
62
63
|
return new Promise((resolve, reject) => {
|
|
63
64
|
const timer = setTimeout(() => {
|
|
64
65
|
bot.removeListener("message", handler);
|
|
65
66
|
reject(new Error("Timed out (5 min)"));
|
|
66
67
|
}, timeoutMs);
|
|
67
|
-
|
|
68
68
|
function handler(msg) {
|
|
69
69
|
if (String(msg.chat.id) !== String(CHAT_ID)) return;
|
|
70
70
|
if (msg.text?.startsWith("/")) return;
|
|
@@ -82,7 +82,6 @@ function waitForCallback(timeoutMs = 300_000) {
|
|
|
82
82
|
bot.removeListener("callback_query", handler);
|
|
83
83
|
reject(new Error("Timed out (5 min)"));
|
|
84
84
|
}, timeoutMs);
|
|
85
|
-
|
|
86
85
|
function handler(query) {
|
|
87
86
|
if (String(query.from.id) !== String(CHAT_ID)) return;
|
|
88
87
|
clearTimeout(timer);
|
|
@@ -94,11 +93,54 @@ function waitForCallback(timeoutMs = 300_000) {
|
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Wait for a message addressed to THIS instance.
|
|
98
|
+
* Format: "@instance-label <instruction>"
|
|
99
|
+
* e.g. "@backend#a3f2 now refactor the auth module"
|
|
100
|
+
*
|
|
101
|
+
* Ignores messages addressed to other instances silently.
|
|
102
|
+
* Times out after `timeoutMs` (default: 1 hour).
|
|
103
|
+
*/
|
|
104
|
+
function waitForAddressedMessage(timeoutMs = 3_600_000) {
|
|
105
|
+
const mention = `@${INSTANCE}`.toLowerCase();
|
|
106
|
+
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
bot.removeListener("message", handler);
|
|
110
|
+
reject(new Error(`No message addressed to ${INSTANCE} within timeout`));
|
|
111
|
+
}, timeoutMs);
|
|
112
|
+
|
|
113
|
+
function handler(msg) {
|
|
114
|
+
if (String(msg.chat.id) !== String(CHAT_ID)) return;
|
|
115
|
+
if (!msg.text) return;
|
|
116
|
+
|
|
117
|
+
const text = msg.text.trim();
|
|
118
|
+
const lower = text.toLowerCase();
|
|
119
|
+
|
|
120
|
+
// Message must start with @instance-label
|
|
121
|
+
if (!lower.startsWith(mention)) return;
|
|
122
|
+
|
|
123
|
+
// Extract the instruction after the mention
|
|
124
|
+
const instruction = text.slice(mention.length).trim();
|
|
125
|
+
if (!instruction) return;
|
|
126
|
+
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
bot.removeListener("message", handler);
|
|
129
|
+
resolve(instruction);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
bot.on("message", handler);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
function terminalPrompt(question) {
|
|
98
137
|
return new Promise((resolve) => {
|
|
99
138
|
const rl = createInterface({ input, output, terminal: true });
|
|
100
139
|
process.stderr.write(`\n[${INSTANCE}] ${question}\n> `);
|
|
101
|
-
rl.once("line", (line) => {
|
|
140
|
+
rl.once("line", (line) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
resolve(line.trim());
|
|
143
|
+
});
|
|
102
144
|
});
|
|
103
145
|
}
|
|
104
146
|
|
|
@@ -113,7 +155,9 @@ function raceCallback(question, options) {
|
|
|
113
155
|
const numbered = options.map((o, i) => ` ${i + 1}. ${o}`).join("\n");
|
|
114
156
|
return Promise.race([
|
|
115
157
|
waitForCallback(),
|
|
116
|
-
terminalPrompt(
|
|
158
|
+
terminalPrompt(
|
|
159
|
+
`${question}\n${numbered}\nChoose (1-${options.length})`,
|
|
160
|
+
).then((v) => {
|
|
117
161
|
const idx = parseInt(v, 10) - 1;
|
|
118
162
|
return { source: "terminal", value: options[idx] ?? v };
|
|
119
163
|
}),
|
|
@@ -124,52 +168,111 @@ function raceCallback(question, options) {
|
|
|
124
168
|
|
|
125
169
|
const server = new McpServer({ name: "telegram-mcp", version: "1.0.0" });
|
|
126
170
|
|
|
171
|
+
// ── telegram_notify ───────────────────────────────────────────────────────────
|
|
172
|
+
|
|
127
173
|
server.tool(
|
|
128
174
|
"telegram_notify",
|
|
129
175
|
"Send a Telegram notification to the user. Use for progress updates and task completions. Does NOT wait for a reply.",
|
|
130
176
|
{ message: z.string().describe("The message to send") },
|
|
131
177
|
async ({ message }) => {
|
|
132
|
-
await bot.sendMessage(CHAT_ID, `${HDR} ${message}`, {
|
|
178
|
+
await bot.sendMessage(CHAT_ID, `${HDR} ${message}`, {
|
|
179
|
+
parse_mode: "Markdown",
|
|
180
|
+
});
|
|
133
181
|
process.stderr.write(`[${INSTANCE}] notify: ${message}\n`);
|
|
134
182
|
return { content: [{ type: "text", text: "Sent." }] };
|
|
135
|
-
}
|
|
183
|
+
},
|
|
136
184
|
);
|
|
137
185
|
|
|
186
|
+
// ── telegram_ask ──────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
138
188
|
server.tool(
|
|
139
189
|
"telegram_ask",
|
|
140
|
-
"Ask the user a free-form question via Telegram and wait for their reply. The same question
|
|
190
|
+
"Ask the user a free-form question via Telegram and wait for their reply. The same question appears on the terminal — whoever answers first wins.",
|
|
141
191
|
{ question: z.string().describe("The question to ask") },
|
|
142
192
|
async ({ question }) => {
|
|
143
|
-
await bot.sendMessage(CHAT_ID, `${HDR} ❓ ${question}`, {
|
|
193
|
+
await bot.sendMessage(CHAT_ID, `${HDR} ❓ ${question}`, {
|
|
194
|
+
parse_mode: "Markdown",
|
|
195
|
+
});
|
|
144
196
|
const { source, value } = await raceReply(question);
|
|
145
197
|
if (source === "terminal") {
|
|
146
|
-
await bot.sendMessage(
|
|
198
|
+
await bot.sendMessage(
|
|
199
|
+
CHAT_ID,
|
|
200
|
+
`${HDR} ✅ Answered from terminal: *${value}*`,
|
|
201
|
+
{ parse_mode: "Markdown" },
|
|
202
|
+
);
|
|
147
203
|
}
|
|
148
204
|
process.stderr.write(`[${INSTANCE}] ask (${source}): ${value}\n`);
|
|
149
205
|
return { content: [{ type: "text", text: value }] };
|
|
150
|
-
}
|
|
206
|
+
},
|
|
151
207
|
);
|
|
152
208
|
|
|
209
|
+
// ── telegram_choose ───────────────────────────────────────────────────────────
|
|
210
|
+
|
|
153
211
|
server.tool(
|
|
154
212
|
"telegram_choose",
|
|
155
213
|
"Ask the user to pick one option. Shows inline buttons on Telegram and a numbered list on the terminal. Whoever responds first wins.",
|
|
156
214
|
{
|
|
157
215
|
question: z.string().describe("The question or prompt"),
|
|
158
|
-
options: z
|
|
216
|
+
options: z
|
|
217
|
+
.array(z.string())
|
|
218
|
+
.min(2)
|
|
219
|
+
.max(10)
|
|
220
|
+
.describe("Options to present (2–10)"),
|
|
159
221
|
},
|
|
160
222
|
async ({ question, options }) => {
|
|
161
223
|
const keyboard = {
|
|
162
|
-
inline_keyboard: options.map((opt) => [
|
|
224
|
+
inline_keyboard: options.map((opt) => [
|
|
225
|
+
{ text: opt, callback_data: opt },
|
|
226
|
+
]),
|
|
163
227
|
};
|
|
164
228
|
await bot.sendMessage(CHAT_ID, `${HDR} 🔘 ${question}`, {
|
|
165
229
|
parse_mode: "Markdown",
|
|
166
230
|
reply_markup: keyboard,
|
|
167
231
|
});
|
|
168
232
|
const { source, value } = await raceCallback(question, options);
|
|
169
|
-
await bot.sendMessage(CHAT_ID, `${HDR} ✅ *${value}* _(via ${source})_`, {
|
|
233
|
+
await bot.sendMessage(CHAT_ID, `${HDR} ✅ *${value}* _(via ${source})_`, {
|
|
234
|
+
parse_mode: "Markdown",
|
|
235
|
+
});
|
|
170
236
|
process.stderr.write(`[${INSTANCE}] choose (${source}): ${value}\n`);
|
|
171
237
|
return { content: [{ type: "text", text: value }] };
|
|
172
|
-
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// ── telegram_listen ───────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
server.tool(
|
|
244
|
+
"telegram_listen",
|
|
245
|
+
`Wait for the user to send a new instruction addressed to this instance on Telegram.
|
|
246
|
+
Call this after completing a task to stay available for follow-up work.
|
|
247
|
+
The user must address messages as: @${INSTANCE} <instruction>
|
|
248
|
+
Returns the instruction text when received. Times out after 1 hour of inactivity.
|
|
249
|
+
When this tool returns, execute the instruction and call telegram_listen again when done.`,
|
|
250
|
+
{},
|
|
251
|
+
async () => {
|
|
252
|
+
await bot.sendMessage(
|
|
253
|
+
CHAT_ID,
|
|
254
|
+
`${HDR} ✅ Task complete — waiting for your next instruction.\n` +
|
|
255
|
+
`_Address me as_ \`@${INSTANCE} <your instruction>\``,
|
|
256
|
+
{ parse_mode: "Markdown" },
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
process.stderr.write(`[${INSTANCE}] listening for @${INSTANCE} ...\n`);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const instruction = await waitForAddressedMessage();
|
|
263
|
+
process.stderr.write(`[${INSTANCE}] received: ${instruction}\n`);
|
|
264
|
+
return { content: [{ type: "text", text: instruction }] };
|
|
265
|
+
} catch (err) {
|
|
266
|
+
await bot.sendMessage(
|
|
267
|
+
CHAT_ID,
|
|
268
|
+
`${HDR} 💤 Timed out after 1 hour of inactivity.`,
|
|
269
|
+
{
|
|
270
|
+
parse_mode: "Markdown",
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
return { content: [{ type: "text", text: `timeout: ${err.message}` }] };
|
|
274
|
+
}
|
|
275
|
+
},
|
|
173
276
|
);
|
|
174
277
|
|
|
175
278
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
@@ -177,3 +280,6 @@ server.tool(
|
|
|
177
280
|
const transport = new StdioServerTransport();
|
|
178
281
|
await server.connect(transport);
|
|
179
282
|
process.stderr.write(`[telegram-mcp] Ready — instance: ${INSTANCE}\n`);
|
|
283
|
+
process.stderr.write(
|
|
284
|
+
`[telegram-mcp] Address messages as: @${INSTANCE} <instruction>\n`,
|
|
285
|
+
);
|
package/src/setup.js
CHANGED
|
@@ -171,6 +171,17 @@ Examples: \`backend#a3f2\`, \`frontend#9c11\`
|
|
|
171
171
|
|
|
172
172
|
Use this exact label in every Telegram message for the entire session.
|
|
173
173
|
Send as first message: \`[label] ✅ Telegram active — ready.\`
|
|
174
|
+
Also tell the user: _Address me as_ \`@[label] <instruction>\` for follow-up tasks.
|
|
175
|
+
|
|
176
|
+
## Staying alive after a task
|
|
177
|
+
|
|
178
|
+
After completing any task, ALWAYS call telegram_listen (no arguments).
|
|
179
|
+
- It will notify the user the task is done and wait for the next instruction
|
|
180
|
+
- When it returns, execute the new instruction
|
|
181
|
+
- Then call telegram_listen again
|
|
182
|
+
- This keeps the session alive indefinitely until timeout (1 hour of inactivity)
|
|
183
|
+
|
|
184
|
+
Never exit or stop after a task — always loop back to telegram_listen.
|
|
174
185
|
`,
|
|
175
186
|
);
|
|
176
187
|
|