@thxmxx/telegram-mcp 1.0.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 ADDED
@@ -0,0 +1,88 @@
1
+ # @thxmxx/telegram-mcp
2
+
3
+ Telegram bridge for Claude Code. While Claude runs tasks on your machine, it notifies you, asks questions and shows option buttons — on your phone **and** on the terminal simultaneously. Whichever you answer first wins.
4
+
5
+ ## Install (one-time, global)
6
+
7
+ ```bash
8
+ npx @thxmxx/telegram-mcp init
9
+ ```
10
+
11
+ The wizard asks for your Telegram bot token and user ID, registers the MCP server globally in Claude Code, and installs the `/use-telegram` slash command. You never need to run this again.
12
+
13
+ ## Usage
14
+
15
+ In any Claude Code session, activate Telegram with the slash command:
16
+
17
+ ```
18
+ /use-telegram
19
+ ```
20
+
21
+ Or just mention it naturally in your prompt:
22
+
23
+ ```
24
+ Refactor the auth module and notify me on Telegram when done.
25
+ Deploy to staging — ask me on Telegram if anything is unclear.
26
+ ```
27
+
28
+ ### Modes
29
+
30
+ ```
31
+ /use-telegram Full mode — notify + ask + choose
32
+ /use-telegram notify Notifications only, no questions
33
+ ```
34
+
35
+ ### Combining with other slash commands
36
+
37
+ Slash commands are independent and composable:
38
+
39
+ ```
40
+ /deploy staging
41
+ /use-telegram notify
42
+ ```
43
+
44
+ ## How it works
45
+
46
+ ```
47
+ Claude Code runs a task
48
+ ↓ calls telegram_choose("Which DB?", ["PostgreSQL", "MySQL", "SQLite"])
49
+ You get buttons on Telegram AND a numbered list on the terminal
50
+ ↓ you tap PostgreSQL on your phone (or type 1 in the terminal)
51
+ Claude receives "PostgreSQL" and continues
52
+ ```
53
+
54
+ Every message is tagged with an auto-generated instance label like `[backend#a3f2]` or `[frontend#9c11]` — so when you have multiple Claude Code sessions open you always know which one is talking.
55
+
56
+ If you answer from the terminal, Telegram confirms it:
57
+ ```
58
+ [backend#a3f2] ✅ PostgreSQL (via terminal)
59
+ ```
60
+
61
+ ## Tools Claude gains
62
+
63
+ | Tool | Description |
64
+ |---|---|
65
+ | `telegram_notify` | Send a progress update. No reply needed. |
66
+ | `telegram_ask` | Ask a free-form question. Waits for reply. |
67
+ | `telegram_choose` | Show option buttons. Waits for a tap. |
68
+
69
+ ## Requirements
70
+
71
+ - Node.js 18+
72
+ - [Claude Code](https://docs.claude.ai/claude-code) installed and logged in
73
+ - A Telegram bot token — get one free from [@BotFather](https://t.me/botfather)
74
+ - Your Telegram user ID — message [@userinfobot](https://t.me/userinfobot)
75
+
76
+ ## Permissions
77
+
78
+ On first use, Claude Code will ask you to approve the three tools this MCP server registers (`telegram_notify`, `telegram_ask`, `telegram_choose`). This is standard Claude Code behaviour — you can review exactly what is being granted before accepting.
79
+
80
+ ## Security
81
+
82
+ - The MCP server only accepts responses from your configured Telegram user ID
83
+ - Credentials are stored in `~/.claude.json` by Claude Code — never in this repo
84
+ - If your token is ever exposed, revoke it immediately via @BotFather `/revoke` then re-run `init`
85
+
86
+ ## License
87
+
88
+ MIT
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@thxmxx/telegram-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Telegram bridge for Claude Code — notify, ask and choose via your phone",
5
+ "type": "module",
6
+ "bin": {
7
+ "telegram-mcp": "./src/cli.js"
8
+ },
9
+ "dependencies": {
10
+ "@modelcontextprotocol/sdk": "^1.0.0",
11
+ "node-telegram-bot-api": "^0.66.0",
12
+ "dotenv": "^16.0.0"
13
+ },
14
+ "engines": { "node": ">=18" },
15
+ "license": "MIT"
16
+ }
package/src/cli.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @thxmxx/telegram-mcp
4
+ *
5
+ * npx @thxmxx/telegram-mcp init — one-time global setup
6
+ * npx @thxmxx/telegram-mcp start — start MCP server manually (for testing)
7
+ */
8
+
9
+ const cmd = process.argv[2];
10
+
11
+ if (cmd === "init") {
12
+ await import("./setup.js");
13
+ } else if (cmd === "start") {
14
+ await import("./index.js");
15
+ } else {
16
+ console.log(`
17
+ @thxmxx/telegram-mcp
18
+
19
+ npx @thxmxx/telegram-mcp init One-time setup — registers MCP in Claude Code globally
20
+ npx @thxmxx/telegram-mcp start Start MCP server manually (for testing)
21
+
22
+ After init, use in any Claude Code session:
23
+ /use-telegram Full mode (notify + ask + choose)
24
+ /use-telegram notify Notifications only
25
+ `);
26
+ process.exit(0);
27
+ }
package/src/index.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @thxmxx/telegram-mcp — MCP server
4
+ *
5
+ * Tools:
6
+ * telegram_notify — send a message (fire and forget)
7
+ * telegram_ask — ask a question, wait for text reply
8
+ * telegram_choose — show buttons, wait for a tap
9
+ *
10
+ * Instance label is auto-generated from cwd + ppid.
11
+ * No per-project configuration needed.
12
+ */
13
+
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import TelegramBot from "node-telegram-bot-api";
17
+ import { readFileSync, existsSync } from "node:fs";
18
+ import { join, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { z } from "zod";
21
+ import { createInterface } from "node:readline/promises";
22
+ import { stdin as input, stdout as output, env, exit } from "node:process";
23
+ import { randomBytes } from "node:crypto";
24
+
25
+ // ── Config ────────────────────────────────────────────────────────────────────
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const envPath = join(__dirname, "../.env");
29
+
30
+ if (existsSync(envPath)) {
31
+ for (const line of readFileSync(envPath, "utf8").split("\n")) {
32
+ const [k, ...v] = line.split("=");
33
+ if (k && v.length && !env[k.trim()]) {
34
+ env[k.trim()] = v.join("=").trim();
35
+ }
36
+ }
37
+ }
38
+
39
+ const TOKEN = env.TELEGRAM_BOT_TOKEN;
40
+ const CHAT_ID = env.TELEGRAM_CHAT_ID;
41
+
42
+ if (!TOKEN || !CHAT_ID) {
43
+ process.stderr.write(
44
+ "[telegram-mcp] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID.\n" +
45
+ " Run: npx @thxmxx/telegram-mcp init\n"
46
+ );
47
+ exit(1);
48
+ }
49
+
50
+ // ── Auto instance label: folder#shortid ───────────────────────────────────────
51
+
52
+ const folder = process.cwd().split("/").pop() || "claude";
53
+ const shortId = randomBytes(2).toString("hex"); // e.g. "a3f2"
54
+ const INSTANCE = `${folder}#${shortId}`;
55
+ const HDR = `\`[${INSTANCE}]\``;
56
+
57
+ // ── Telegram client ───────────────────────────────────────────────────────────
58
+
59
+ const bot = new TelegramBot(TOKEN, { polling: true });
60
+
61
+ function waitForReply(timeoutMs = 300_000) {
62
+ return new Promise((resolve, reject) => {
63
+ const timer = setTimeout(() => {
64
+ bot.removeListener("message", handler);
65
+ reject(new Error("Timed out (5 min)"));
66
+ }, timeoutMs);
67
+
68
+ function handler(msg) {
69
+ if (String(msg.chat.id) !== String(CHAT_ID)) return;
70
+ if (msg.text?.startsWith("/")) return;
71
+ clearTimeout(timer);
72
+ bot.removeListener("message", handler);
73
+ resolve({ source: "telegram", value: msg.text || "" });
74
+ }
75
+ bot.on("message", handler);
76
+ });
77
+ }
78
+
79
+ function waitForCallback(timeoutMs = 300_000) {
80
+ return new Promise((resolve, reject) => {
81
+ const timer = setTimeout(() => {
82
+ bot.removeListener("callback_query", handler);
83
+ reject(new Error("Timed out (5 min)"));
84
+ }, timeoutMs);
85
+
86
+ function handler(query) {
87
+ if (String(query.from.id) !== String(CHAT_ID)) return;
88
+ clearTimeout(timer);
89
+ bot.removeListener("callback_query", handler);
90
+ bot.answerCallbackQuery(query.id);
91
+ resolve({ source: "telegram", value: query.data || "" });
92
+ }
93
+ bot.on("callback_query", handler);
94
+ });
95
+ }
96
+
97
+ function terminalPrompt(question) {
98
+ return new Promise((resolve) => {
99
+ const rl = createInterface({ input, output, terminal: true });
100
+ process.stderr.write(`\n[${INSTANCE}] ${question}\n> `);
101
+ rl.once("line", (line) => { rl.close(); resolve(line.trim()); });
102
+ });
103
+ }
104
+
105
+ function raceReply(question) {
106
+ return Promise.race([
107
+ waitForReply(),
108
+ terminalPrompt(question).then((v) => ({ source: "terminal", value: v })),
109
+ ]);
110
+ }
111
+
112
+ function raceCallback(question, options) {
113
+ const numbered = options.map((o, i) => ` ${i + 1}. ${o}`).join("\n");
114
+ return Promise.race([
115
+ waitForCallback(),
116
+ terminalPrompt(`${question}\n${numbered}\nChoose (1-${options.length})`).then((v) => {
117
+ const idx = parseInt(v, 10) - 1;
118
+ return { source: "terminal", value: options[idx] ?? v };
119
+ }),
120
+ ]);
121
+ }
122
+
123
+ // ── MCP server ────────────────────────────────────────────────────────────────
124
+
125
+ const server = new McpServer({ name: "telegram-mcp", version: "1.0.0" });
126
+
127
+ server.tool(
128
+ "telegram_notify",
129
+ "Send a Telegram notification to the user. Use for progress updates and task completions. Does NOT wait for a reply.",
130
+ { message: z.string().describe("The message to send") },
131
+ async ({ message }) => {
132
+ await bot.sendMessage(CHAT_ID, `${HDR} ${message}`, { parse_mode: "Markdown" });
133
+ process.stderr.write(`[${INSTANCE}] notify: ${message}\n`);
134
+ return { content: [{ type: "text", text: "Sent." }] };
135
+ }
136
+ );
137
+
138
+ server.tool(
139
+ "telegram_ask",
140
+ "Ask the user a free-form question via Telegram and wait for their reply. The same question is shown on the terminal — whoever answers first wins.",
141
+ { question: z.string().describe("The question to ask") },
142
+ async ({ question }) => {
143
+ await bot.sendMessage(CHAT_ID, `${HDR} ❓ ${question}`, { parse_mode: "Markdown" });
144
+ const { source, value } = await raceReply(question);
145
+ if (source === "terminal") {
146
+ await bot.sendMessage(CHAT_ID, `${HDR} ✅ Answered from terminal: *${value}*`, { parse_mode: "Markdown" });
147
+ }
148
+ process.stderr.write(`[${INSTANCE}] ask (${source}): ${value}\n`);
149
+ return { content: [{ type: "text", text: value }] };
150
+ }
151
+ );
152
+
153
+ server.tool(
154
+ "telegram_choose",
155
+ "Ask the user to pick one option. Shows inline buttons on Telegram and a numbered list on the terminal. Whoever responds first wins.",
156
+ {
157
+ question: z.string().describe("The question or prompt"),
158
+ options: z.array(z.string()).min(2).max(10).describe("Options to present (2–10)"),
159
+ },
160
+ async ({ question, options }) => {
161
+ const keyboard = {
162
+ inline_keyboard: options.map((opt) => [{ text: opt, callback_data: opt }]),
163
+ };
164
+ await bot.sendMessage(CHAT_ID, `${HDR} 🔘 ${question}`, {
165
+ parse_mode: "Markdown",
166
+ reply_markup: keyboard,
167
+ });
168
+ const { source, value } = await raceCallback(question, options);
169
+ await bot.sendMessage(CHAT_ID, `${HDR} ✅ *${value}* _(via ${source})_`, { parse_mode: "Markdown" });
170
+ process.stderr.write(`[${INSTANCE}] choose (${source}): ${value}\n`);
171
+ return { content: [{ type: "text", text: value }] };
172
+ }
173
+ );
174
+
175
+ // ── Start ─────────────────────────────────────────────────────────────────────
176
+
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ process.stderr.write(`[telegram-mcp] Ready — instance: ${INSTANCE}\n`);
package/src/setup.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npx @thxmxx/telegram-mcp init
4
+ *
5
+ * One-time global setup:
6
+ * 1. Asks for bot token and chat ID
7
+ * 2. Registers the MCP server globally in Claude Code (~/.claude.json)
8
+ * 3. Installs the /use-telegram slash command in ~/.claude/commands/
9
+ *
10
+ * Instance names are auto-generated per session — no need to configure them.
11
+ */
12
+
13
+ import { createInterface } from "node:readline/promises";
14
+ import { stdin as input, stdout as output, exit } from "node:process";
15
+ import { execFileSync } from "node:child_process";
16
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+
20
+ const rl = createInterface({ input, output });
21
+ const ask = (q) => rl.question(q);
22
+
23
+ const BOLD = "\x1b[1m";
24
+ const GREEN = "\x1b[32m";
25
+ const CYAN = "\x1b[36m";
26
+ const DIM = "\x1b[2m";
27
+ const RESET = "\x1b[0m";
28
+
29
+ const ok = (msg) => console.log(`${GREEN}✔${RESET} ${msg}`);
30
+ const die = (msg) => { console.error(`\x1b[31m✘${RESET} ${msg}`); rl.close(); exit(1); };
31
+
32
+ // ── Pre-flight ────────────────────────────────────────────────────────────────
33
+
34
+ console.log(`\n${BOLD}@thxmxx/telegram-mcp — one-time setup${RESET}\n`);
35
+
36
+ const [major] = process.versions.node.split(".").map(Number);
37
+ if (major < 18) die(`Node.js 18+ required (you have ${process.versions.node})`);
38
+ ok(`Node.js ${process.versions.node}`);
39
+
40
+ try {
41
+ const ver = execFileSync("claude", ["--version"], { encoding: "utf8" }).trim();
42
+ ok(`Claude Code: ${ver}`);
43
+ } catch {
44
+ die("`claude` not found. Install: https://docs.claude.ai/claude-code");
45
+ }
46
+
47
+ // ── Step 1: Bot token ─────────────────────────────────────────────────────────
48
+
49
+ console.log(`
50
+ ${BOLD}Step 1 — Bot token${RESET}
51
+ ${DIM}Open Telegram → @BotFather → /newbot${RESET}
52
+ `);
53
+ const token = (await ask(" Bot token: ")).trim();
54
+ if (!token) die("Token is required.");
55
+
56
+ // ── Step 2: Chat ID ───────────────────────────────────────────────────────────
57
+
58
+ console.log(`
59
+ ${BOLD}Step 2 — Your Telegram user ID${RESET}
60
+ ${DIM}Message @userinfobot on Telegram to get your numeric ID.${RESET}
61
+ `);
62
+ const chatId = (await ask(" Your Telegram user ID: ")).trim();
63
+ if (!chatId) die("User ID is required.");
64
+
65
+ rl.close();
66
+
67
+ // ── Register MCP globally in Claude Code ─────────────────────────────────────
68
+
69
+ console.log(`\n${BOLD}Registering MCP server globally…${RESET}`);
70
+
71
+ const serverPath = new URL("./index.js", import.meta.url).pathname;
72
+
73
+ try {
74
+ execFileSync("claude", [
75
+ "mcp", "add", "--scope", "user", // global, persists across all projects
76
+ "telegram-mcp",
77
+ "-e", `TELEGRAM_BOT_TOKEN=${token}`,
78
+ "-e", `TELEGRAM_CHAT_ID=${chatId}`,
79
+ "--", "node", serverPath,
80
+ ], { stdio: "inherit" });
81
+ } catch {
82
+ // Fallback: older Claude Code versions without --scope flag
83
+ try {
84
+ execFileSync("claude", [
85
+ "mcp", "add",
86
+ "telegram-mcp",
87
+ "-e", `TELEGRAM_BOT_TOKEN=${token}`,
88
+ "-e", `TELEGRAM_CHAT_ID=${chatId}`,
89
+ "--", "node", serverPath,
90
+ ], { stdio: "inherit" });
91
+ } catch {
92
+ die(
93
+ "`claude mcp add` failed.\n" +
94
+ " Update Claude Code: https://docs.claude.ai/claude-code\n\n" +
95
+ " Manual config:\n" +
96
+ ` TELEGRAM_BOT_TOKEN=${token}\n` +
97
+ ` TELEGRAM_CHAT_ID=${chatId}\n` +
98
+ ` command: node ${serverPath}`
99
+ );
100
+ }
101
+ }
102
+
103
+ ok("MCP server registered globally.");
104
+
105
+ // ── Install /use-telegram slash command ───────────────────────────────────────
106
+
107
+ const commandsDir = join(homedir(), ".claude", "commands");
108
+ mkdirSync(commandsDir, { recursive: true });
109
+
110
+ const commandPath = join(commandsDir, "use-telegram.md");
111
+ writeFileSync(commandPath, `# use-telegram
112
+
113
+ Activate Telegram integration for this Claude Code session.
114
+
115
+ ## What this does
116
+
117
+ Enables the telegram_notify, telegram_ask and telegram_choose tools so you
118
+ can be notified and asked questions on your phone while Claude works.
119
+
120
+ The instance name is derived automatically from the current folder and
121
+ session ID, so messages on Telegram always show which session is talking.
122
+
123
+ ## Modes
124
+
125
+ - \`/use-telegram\` — full interactive mode (notify + ask + choose)
126
+ - \`/use-telegram notify\` — notifications only, no questions
127
+
128
+ ## Behaviour
129
+
130
+ When this command is active:
131
+ - Use telegram_notify for progress updates and completed tasks
132
+ - Use telegram_ask when you need a free-form answer from the user
133
+ - Use telegram_choose when the user must pick from known options
134
+ - Always prefix messages with the auto-generated instance label
135
+ - Mirror every question to the terminal too — whoever answers first wins
136
+ - After any long-running task, send a completion summary via telegram_notify
137
+
138
+ ## Instance naming
139
+
140
+ Generate the instance label as: \`{folder-name}#{short-session-id}\`
141
+ Example: \`backend#a3f2\`, \`frontend#9c11\`
142
+
143
+ Use the same label for the entire session.
144
+ `);
145
+
146
+ ok(`Slash command installed → ${commandPath}`);
147
+
148
+ // ── Done ──────────────────────────────────────────────────────────────────────
149
+
150
+ console.log(`
151
+ ${GREEN}${BOLD}All done!${RESET}
152
+
153
+ Restart Claude Code. From now on, in any session:
154
+
155
+ ${BOLD}/use-telegram${RESET} full mode (notify + ask + choose)
156
+ ${BOLD}/use-telegram notify${RESET} notifications only
157
+
158
+ Or just tell Claude in the prompt:
159
+ ${DIM}"refactor this and notify me on telegram when done"${RESET}
160
+ ${DIM}"deploy to staging, ask me via telegram if anything is unclear"${RESET}
161
+
162
+ Each session auto-identifies itself as ${BOLD}[folder#id]${RESET} in Telegram.
163
+ ${DIM}No need to run init again unless you change your bot token.${RESET}
164
+ `);