telegram-notifier-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,192 @@
1
+ # Telegram Notifier MCP Server
2
+
3
+ A one-way notification [MCP](https://modelcontextprotocol.io/) server that lets an LLM send messages and files to a user via a Telegram bot. No external HTTP or Telegram libraries — just the native `fetch` API and the official MCP SDK.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - A Telegram bot token (from [@BotFather](https://t.me/BotFather))
9
+ - Your Telegram chat ID
10
+
11
+ ## Setup
12
+
13
+ ### 1. Create a Telegram Bot
14
+
15
+ 1. Open Telegram and message [@BotFather](https://t.me/BotFather)
16
+ 2. Send `/newbot` and follow the prompts to name your bot
17
+ 3. Copy the **bot token** you receive (e.g., `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)
18
+
19
+ ### 2. Find Your Chat ID
20
+
21
+ 1. Send any message to your new bot on Telegram
22
+ 2. Open the following URL in your browser, replacing `YOUR_BOT_TOKEN` with your actual token:
23
+ ```
24
+ https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates
25
+ ```
26
+ 3. In the JSON response, find `"chat":{"id": 123456789}` — that number is your chat ID
27
+
28
+ > **Tip:** For group chats, add the bot to the group, send a message, and check the same URL. Group chat IDs are negative numbers (e.g., `-1001234567890`).
29
+
30
+ ### 3. Install and Build
31
+
32
+ ```bash
33
+ git clone <your-repo-url>
34
+ cd telegram-notifier-mcp
35
+ npm install
36
+ npm run build
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ The server uses two environment variables:
42
+
43
+ | Variable | Required | Description |
44
+ |---|---|---|
45
+ | `TELEGRAM_BOT_TOKEN` | Yes | Bot token from @BotFather |
46
+ | `TELEGRAM_CHAT_ID` | No | Default chat ID. Can be overridden per-tool call via the `chatId` parameter. |
47
+
48
+ The server will exit with an error if `TELEGRAM_BOT_TOKEN` is not set. If `TELEGRAM_CHAT_ID` is not set, you must pass `chatId` to every tool call.
49
+
50
+ ## Adding to Your MCP Client
51
+
52
+ ### Claude Desktop
53
+
54
+ Add this to your Claude Desktop config file:
55
+
56
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
57
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "telegram-notifier": {
63
+ "command": "node",
64
+ "args": ["/absolute/path/to/telegram-notifier-mcp/build/index.js"],
65
+ "env": {
66
+ "TELEGRAM_BOT_TOKEN": "your-bot-token-here",
67
+ "TELEGRAM_CHAT_ID": "your-chat-id-here"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Claude Code
75
+
76
+ Add to your project's `.mcp.json` or global `~/.claude/claude_code_config.json`:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "telegram-notifier": {
82
+ "command": "node",
83
+ "args": ["/absolute/path/to/telegram-notifier-mcp/build/index.js"],
84
+ "env": {
85
+ "TELEGRAM_BOT_TOKEN": "your-bot-token-here",
86
+ "TELEGRAM_CHAT_ID": "your-chat-id-here"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Tools
94
+
95
+ ### `send_message`
96
+
97
+ Send a text message to a Telegram chat.
98
+
99
+ | Parameter | Type | Required | Description |
100
+ |---|---|---|---|
101
+ | `text` | string | Yes | The message text to send |
102
+ | `chatId` | string | No | Target chat ID (overrides `TELEGRAM_CHAT_ID`) |
103
+ | `parseMode` | string | No | `Markdown`, `MarkdownV2`, or `HTML` |
104
+ | `disableNotification` | boolean | No | Send silently without notification sound |
105
+
106
+ ### `send_document`
107
+
108
+ Send a file/document to a Telegram chat.
109
+
110
+ | Parameter | Type | Required | Description |
111
+ |---|---|---|---|
112
+ | `filePath` | string | Yes | Absolute path to the file |
113
+ | `chatId` | string | No | Target chat ID (overrides `TELEGRAM_CHAT_ID`) |
114
+ | `caption` | string | No | Caption for the document |
115
+ | `parseMode` | string | No | `Markdown`, `MarkdownV2`, or `HTML` |
116
+ | `disableNotification` | boolean | No | Send silently without notification sound |
117
+
118
+ ### `send_photo`
119
+
120
+ Send a photo/image to a Telegram chat.
121
+
122
+ | Parameter | Type | Required | Description |
123
+ |---|---|---|---|
124
+ | `filePath` | string | Yes | Absolute path to the image file |
125
+ | `chatId` | string | No | Target chat ID (overrides `TELEGRAM_CHAT_ID`) |
126
+ | `caption` | string | No | Caption for the photo |
127
+ | `parseMode` | string | No | `Markdown`, `MarkdownV2`, or `HTML` |
128
+ | `disableNotification` | boolean | No | Send silently without notification sound |
129
+
130
+ ### `send_video`
131
+
132
+ Send a video to a Telegram chat.
133
+
134
+ | Parameter | Type | Required | Description |
135
+ |---|---|---|---|
136
+ | `filePath` | string | Yes | Absolute path to the video file |
137
+ | `chatId` | string | No | Target chat ID (overrides `TELEGRAM_CHAT_ID`) |
138
+ | `caption` | string | No | Caption for the video |
139
+ | `parseMode` | string | No | `Markdown`, `MarkdownV2`, or `HTML` |
140
+ | `disableNotification` | boolean | No | Send silently without notification sound |
141
+
142
+ ### `send_audio`
143
+
144
+ Send an audio file to a Telegram chat.
145
+
146
+ | Parameter | Type | Required | Description |
147
+ |---|---|---|---|
148
+ | `filePath` | string | Yes | Absolute path to the audio file |
149
+ | `chatId` | string | No | Target chat ID (overrides `TELEGRAM_CHAT_ID`) |
150
+ | `caption` | string | No | Caption for the audio |
151
+ | `parseMode` | string | No | `Markdown`, `MarkdownV2`, or `HTML` |
152
+ | `disableNotification` | boolean | No | Send silently without notification sound |
153
+
154
+ ## Testing with the MCP Inspector
155
+
156
+ You can test the server interactively using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
157
+
158
+ ```bash
159
+ TELEGRAM_BOT_TOKEN="your-token" TELEGRAM_CHAT_ID="your-chat-id" \
160
+ npx @modelcontextprotocol/inspector node build/index.js
161
+ ```
162
+
163
+ This opens a browser UI where you can invoke each tool and see the results.
164
+
165
+ ## Error Handling
166
+
167
+ The server handles errors gracefully and returns descriptive messages:
168
+
169
+ | Scenario | Behavior |
170
+ |---|---|
171
+ | Missing `TELEGRAM_BOT_TOKEN` | Server exits at startup with instructions |
172
+ | Missing chat ID (no env var, no parameter) | Returns `isError: true` with message |
173
+ | File not found | Returns `isError: true` with the file path |
174
+ | File exceeds 50 MB | Returns `isError: true` with file size |
175
+ | Telegram API error | Returns `isError: true` with Telegram's error description |
176
+
177
+ All server logs go to **stderr** so they never interfere with the stdio MCP transport on stdout.
178
+
179
+ ## File Size Limits
180
+
181
+ Telegram enforces a **50 MB** limit for file uploads via the Bot API. The server validates file size before uploading and returns an error if the limit is exceeded.
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ # Watch mode — rebuilds on file changes
187
+ npm run dev
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/index.js ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { readFile, stat } from "node:fs/promises";
6
+ import { basename } from "node:path";
7
+ // ---------------------------------------------------------------------------
8
+ // Config
9
+ // ---------------------------------------------------------------------------
10
+ const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
11
+ const DEFAULT_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
12
+ if (!TELEGRAM_BOT_TOKEN) {
13
+ console.error("Error: TELEGRAM_BOT_TOKEN environment variable is required.\n" +
14
+ "Get a token from @BotFather on Telegram.");
15
+ process.exit(1);
16
+ }
17
+ const TELEGRAM_API = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}`;
18
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB
19
+ async function sendMessage(chatId, text, parseMode, disableNotification) {
20
+ const body = { chat_id: chatId, text };
21
+ if (parseMode)
22
+ body.parse_mode = parseMode;
23
+ if (disableNotification)
24
+ body.disable_notification = true;
25
+ const res = await fetch(`${TELEGRAM_API}/sendMessage`, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ });
30
+ return (await res.json());
31
+ }
32
+ const FILE_FIELD = {
33
+ sendDocument: "document",
34
+ sendPhoto: "photo",
35
+ sendVideo: "video",
36
+ sendAudio: "audio",
37
+ };
38
+ async function sendFile(method, chatId, filePath, caption, parseMode, disableNotification) {
39
+ // Validate file exists and is within size limit
40
+ let fileStats;
41
+ try {
42
+ fileStats = await stat(filePath);
43
+ }
44
+ catch {
45
+ return { ok: false, description: `File not found: ${filePath}` };
46
+ }
47
+ if (fileStats.size > MAX_FILE_SIZE) {
48
+ return {
49
+ ok: false,
50
+ description: `File exceeds 50 MB limit (${(fileStats.size / 1024 / 1024).toFixed(1)} MB): ${filePath}`,
51
+ };
52
+ }
53
+ const fileBuffer = await readFile(filePath);
54
+ const blob = new Blob([fileBuffer]);
55
+ const fileName = basename(filePath);
56
+ const form = new FormData();
57
+ form.append("chat_id", chatId);
58
+ form.append(FILE_FIELD[method], blob, fileName);
59
+ if (caption)
60
+ form.append("caption", caption);
61
+ if (parseMode)
62
+ form.append("parse_mode", parseMode);
63
+ if (disableNotification)
64
+ form.append("disable_notification", "true");
65
+ const res = await fetch(`${TELEGRAM_API}/${method}`, {
66
+ method: "POST",
67
+ body: form,
68
+ });
69
+ return (await res.json());
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Helpers
73
+ // ---------------------------------------------------------------------------
74
+ function resolveChatId(overrideChatId) {
75
+ return overrideChatId || DEFAULT_CHAT_ID || null;
76
+ }
77
+ function successResult(text) {
78
+ return { content: [{ type: "text", text }] };
79
+ }
80
+ function errorResult(text) {
81
+ return { content: [{ type: "text", text }], isError: true };
82
+ }
83
+ function telegramResult(res, successMsg) {
84
+ if (res.ok)
85
+ return successResult(successMsg);
86
+ return errorResult(`Telegram API error: ${res.description ?? "Unknown error"}`);
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // MCP Server
90
+ // ---------------------------------------------------------------------------
91
+ const server = new McpServer({
92
+ name: "telegram-notifier",
93
+ version: "1.0.0",
94
+ });
95
+ // -- send_message -----------------------------------------------------------
96
+ server.tool("send_message", "Send a text message to a Telegram chat", {
97
+ text: z.string().describe("The message text to send"),
98
+ chatId: z
99
+ .string()
100
+ .optional()
101
+ .describe("Target chat ID (overrides TELEGRAM_CHAT_ID env var)"),
102
+ parseMode: z
103
+ .enum(["Markdown", "MarkdownV2", "HTML"])
104
+ .optional()
105
+ .describe("Message formatting mode"),
106
+ disableNotification: z
107
+ .boolean()
108
+ .optional()
109
+ .describe("Send silently without notification sound"),
110
+ }, async ({ text, chatId, parseMode, disableNotification }) => {
111
+ const resolvedChatId = resolveChatId(chatId);
112
+ if (!resolvedChatId) {
113
+ return errorResult("No chat ID provided. Set TELEGRAM_CHAT_ID env var or pass chatId parameter.");
114
+ }
115
+ const res = await sendMessage(resolvedChatId, text, parseMode, disableNotification);
116
+ return telegramResult(res, `Message sent to chat ${resolvedChatId}.`);
117
+ });
118
+ // -- send_document ----------------------------------------------------------
119
+ server.tool("send_document", "Send a file/document to a Telegram chat", {
120
+ filePath: z.string().describe("Absolute path to the file to send"),
121
+ chatId: z
122
+ .string()
123
+ .optional()
124
+ .describe("Target chat ID (overrides TELEGRAM_CHAT_ID env var)"),
125
+ caption: z.string().optional().describe("Caption for the document"),
126
+ parseMode: z
127
+ .enum(["Markdown", "MarkdownV2", "HTML"])
128
+ .optional()
129
+ .describe("Caption formatting mode"),
130
+ disableNotification: z
131
+ .boolean()
132
+ .optional()
133
+ .describe("Send silently without notification sound"),
134
+ }, async ({ filePath, chatId, caption, parseMode, disableNotification }) => {
135
+ const resolvedChatId = resolveChatId(chatId);
136
+ if (!resolvedChatId) {
137
+ return errorResult("No chat ID provided. Set TELEGRAM_CHAT_ID env var or pass chatId parameter.");
138
+ }
139
+ const res = await sendFile("sendDocument", resolvedChatId, filePath, caption, parseMode, disableNotification);
140
+ return telegramResult(res, `Document sent to chat ${resolvedChatId}.`);
141
+ });
142
+ // -- send_photo -------------------------------------------------------------
143
+ server.tool("send_photo", "Send a photo/image to a Telegram chat", {
144
+ filePath: z.string().describe("Absolute path to the image file to send"),
145
+ chatId: z
146
+ .string()
147
+ .optional()
148
+ .describe("Target chat ID (overrides TELEGRAM_CHAT_ID env var)"),
149
+ caption: z.string().optional().describe("Caption for the photo"),
150
+ parseMode: z
151
+ .enum(["Markdown", "MarkdownV2", "HTML"])
152
+ .optional()
153
+ .describe("Caption formatting mode"),
154
+ disableNotification: z
155
+ .boolean()
156
+ .optional()
157
+ .describe("Send silently without notification sound"),
158
+ }, async ({ filePath, chatId, caption, parseMode, disableNotification }) => {
159
+ const resolvedChatId = resolveChatId(chatId);
160
+ if (!resolvedChatId) {
161
+ return errorResult("No chat ID provided. Set TELEGRAM_CHAT_ID env var or pass chatId parameter.");
162
+ }
163
+ const res = await sendFile("sendPhoto", resolvedChatId, filePath, caption, parseMode, disableNotification);
164
+ return telegramResult(res, `Photo sent to chat ${resolvedChatId}.`);
165
+ });
166
+ // -- send_video -------------------------------------------------------------
167
+ server.tool("send_video", "Send a video to a Telegram chat", {
168
+ filePath: z.string().describe("Absolute path to the video file to send"),
169
+ chatId: z
170
+ .string()
171
+ .optional()
172
+ .describe("Target chat ID (overrides TELEGRAM_CHAT_ID env var)"),
173
+ caption: z.string().optional().describe("Caption for the video"),
174
+ parseMode: z
175
+ .enum(["Markdown", "MarkdownV2", "HTML"])
176
+ .optional()
177
+ .describe("Caption formatting mode"),
178
+ disableNotification: z
179
+ .boolean()
180
+ .optional()
181
+ .describe("Send silently without notification sound"),
182
+ }, async ({ filePath, chatId, caption, parseMode, disableNotification }) => {
183
+ const resolvedChatId = resolveChatId(chatId);
184
+ if (!resolvedChatId) {
185
+ return errorResult("No chat ID provided. Set TELEGRAM_CHAT_ID env var or pass chatId parameter.");
186
+ }
187
+ const res = await sendFile("sendVideo", resolvedChatId, filePath, caption, parseMode, disableNotification);
188
+ return telegramResult(res, `Video sent to chat ${resolvedChatId}.`);
189
+ });
190
+ // -- send_audio -------------------------------------------------------------
191
+ server.tool("send_audio", "Send an audio file to a Telegram chat", {
192
+ filePath: z.string().describe("Absolute path to the audio file to send"),
193
+ chatId: z
194
+ .string()
195
+ .optional()
196
+ .describe("Target chat ID (overrides TELEGRAM_CHAT_ID env var)"),
197
+ caption: z.string().optional().describe("Caption for the audio"),
198
+ parseMode: z
199
+ .enum(["Markdown", "MarkdownV2", "HTML"])
200
+ .optional()
201
+ .describe("Caption formatting mode"),
202
+ disableNotification: z
203
+ .boolean()
204
+ .optional()
205
+ .describe("Send silently without notification sound"),
206
+ }, async ({ filePath, chatId, caption, parseMode, disableNotification }) => {
207
+ const resolvedChatId = resolveChatId(chatId);
208
+ if (!resolvedChatId) {
209
+ return errorResult("No chat ID provided. Set TELEGRAM_CHAT_ID env var or pass chatId parameter.");
210
+ }
211
+ const res = await sendFile("sendAudio", resolvedChatId, filePath, caption, parseMode, disableNotification);
212
+ return telegramResult(res, `Audio sent to chat ${resolvedChatId}.`);
213
+ });
214
+ // ---------------------------------------------------------------------------
215
+ // Start
216
+ // ---------------------------------------------------------------------------
217
+ async function main() {
218
+ const transport = new StdioServerTransport();
219
+ await server.connect(transport);
220
+ console.error("Telegram Notifier MCP server running on stdio");
221
+ }
222
+ main().catch((error) => {
223
+ console.error("Fatal error:", error);
224
+ process.exit(1);
225
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "telegram-notifier-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for sending notifications via Telegram Bot API",
5
+ "type": "module",
6
+ "bin": {
7
+ "telegram-notifier-mcp": "build/index.js"
8
+ },
9
+ "files": [
10
+ "build"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "zod": "^3.24.2"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.7.3",
23
+ "@types/node": "^22.13.4"
24
+ }
25
+ }