@overpod/mcp-telegram 1.11.0 → 1.12.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.
Files changed (3) hide show
  1. package/README.md +30 -2
  2. package/dist/index.js +381 -275
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@overpod/mcp-telegram)](https://www.npmjs.com/package/@overpod/mcp-telegram)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@overpod/mcp-telegram)](https://www.npmjs.com/package/@overpod/mcp-telegram)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
7
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.27-green.svg)](https://modelcontextprotocol.io/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
9
  [![mcp-telegram MCP server](https://glama.ai/mcp/servers/overpod/mcp-telegram/badges/score.svg)](https://glama.ai/mcp/servers/overpod/mcp-telegram)
@@ -208,6 +208,7 @@ const telegramMcp = new MCPClient({
208
208
  | `telegram-read-messages` | Read recent messages from a chat |
209
209
  | `telegram-search-chats` | Search for chats, users, or channels by name |
210
210
  | `telegram-search-messages` | Search messages in a chat by text |
211
+ | `telegram-search-global` | Search messages across all public chats and channels |
211
212
  | `telegram-get-unread` | Get chats with unread messages; forums show per-topic unread breakdown |
212
213
  | `telegram-get-contact-requests` | Get incoming messages from non-contacts with preview |
213
214
 
@@ -243,6 +244,8 @@ const telegramMcp = new MCPClient({
243
244
  |------|-------------|
244
245
  | `telegram-get-contacts` | Get your contacts list with phone numbers |
245
246
  | `telegram-get-profile` | Get detailed profile info for a user (bio, photo, last seen) |
247
+ | `telegram-get-profile-photo` | Get a user's or chat's profile photo (inline or file) |
248
+ | `telegram-get-reactions` | Get reactions on a specific message with user details |
246
249
 
247
250
  ### Media
248
251
 
@@ -357,7 +360,8 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
357
360
  |-----------|------|----------|-------------|
358
361
  | `chatId` | string | yes | Chat ID or @username |
359
362
  | `messageId` | number | yes | Message ID to react to |
360
- | `emoji` | string | no | Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction |
363
+ | `emoji` | string \| string[] | no | Single emoji `"👍"` or array `["👍","🔥"]`. Omit to remove all reactions |
364
+ | `addToExisting` | boolean | no | Add to existing reactions instead of replacing (default: false) |
361
365
 
362
366
  ### telegram-send-scheduled
363
367
 
@@ -388,6 +392,15 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
388
392
  | `query` | string | yes | Search text |
389
393
  | `limit` | number | no | Max results (default: 20) |
390
394
 
395
+ ### telegram-search-global
396
+
397
+ | Parameter | Type | Required | Description |
398
+ |-----------|------|----------|-------------|
399
+ | `query` | string | yes | Search text |
400
+ | `limit` | number | no | Max results (default: 20) |
401
+ | `minDate` | number | no | Unix timestamp: only messages after this date |
402
+ | `maxDate` | number | no | Unix timestamp: only messages before this date |
403
+
391
404
  ### telegram-search-chats
392
405
 
393
406
  | Parameter | Type | Required | Description |
@@ -414,6 +427,21 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
414
427
  |-----------|------|----------|-------------|
415
428
  | `userId` | string | yes | User ID or @username |
416
429
 
430
+ ### telegram-get-profile-photo
431
+
432
+ | Parameter | Type | Required | Description |
433
+ |-----------|------|----------|-------------|
434
+ | `entityId` | string | yes | User/Chat/Channel ID or username |
435
+ | `savePath` | string | no | Absolute path to save file. If omitted, returns inline base64 image |
436
+ | `size` | `"small"` / `"big"` | no | Photo size: small (160x160) or big (640x640). Default: big |
437
+
438
+ ### telegram-get-reactions
439
+
440
+ | Parameter | Type | Required | Description |
441
+ |-----------|------|----------|-------------|
442
+ | `chatId` | string | yes | Chat ID or @username |
443
+ | `messageId` | number | yes | Message ID to get reactions for |
444
+
417
445
  ### telegram-get-unread
418
446
 
419
447
  | Parameter | Type | Required | Description |
package/dist/index.js CHANGED
@@ -18,6 +18,10 @@ if (!API_ID || !API_HASH) {
18
18
  process.exit(1);
19
19
  }
20
20
  const telegram = new TelegramService(API_ID, API_HASH);
21
+ /** Remove unpaired UTF-16 surrogates that break JSON serialization */
22
+ function sanitize(text) {
23
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
24
+ }
21
25
  /** Format reactions array into compact text like: [👍×5 ❤️×3(me) 🔥×1] */
22
26
  function formatReactions(reactions) {
23
27
  if (!reactions?.length)
@@ -36,30 +40,36 @@ async function requireConnection() {
36
40
  const reason = telegram.lastError ? ` ${telegram.lastError}` : "";
37
41
  return `Not connected to Telegram.${reason} Run telegram-login first.`;
38
42
  }
43
+ /** MCP tool annotation presets */
44
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
45
+ const WRITE = { readOnlyHint: false, openWorldHint: true };
46
+ const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
47
+ /** Helper: success response */
48
+ function ok(text) {
49
+ return { content: [{ type: "text", text }] };
50
+ }
51
+ /** Helper: error response with isError flag */
52
+ function fail(e) {
53
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
54
+ }
39
55
  // --- Tools ---
40
- server.tool("telegram-status", "Check Telegram connection status", {}, async () => {
56
+ server.registerTool("telegram-status", { description: "Check Telegram connection status", annotations: READ_ONLY }, async () => {
41
57
  if (await telegram.ensureConnected()) {
42
58
  try {
43
59
  const me = await telegram.getMe();
44
- return {
45
- content: [
46
- {
47
- type: "text",
48
- text: `Connected as ${me.firstName ?? ""} (@${me.username ?? "unknown"}, id: ${me.id})`,
49
- },
50
- ],
51
- };
60
+ return ok(`Connected as ${me.firstName ?? ""} (@${me.username ?? "unknown"}, id: ${me.id})`);
52
61
  }
53
62
  catch {
54
- return { content: [{ type: "text", text: "Connected, but failed to get user info" }] };
63
+ return ok("Connected, but failed to get user info");
55
64
  }
56
65
  }
57
66
  const reason = telegram.lastError ? ` Reason: ${telegram.lastError}` : "";
58
- return {
59
- content: [{ type: "text", text: `Not connected.${reason} Use telegram-login to authenticate via QR code.` }],
60
- };
67
+ return ok(`Not connected.${reason} Use telegram-login to authenticate via QR code.`);
61
68
  });
62
- server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image. IMPORTANT: pass the entire result to user without modifications.", {}, async () => {
69
+ server.registerTool("telegram-login", {
70
+ description: "Login to Telegram via QR code. Returns QR image. IMPORTANT: pass the entire result to user without modifications.",
71
+ annotations: WRITE,
72
+ }, async () => {
63
73
  let qrDataUrl = "";
64
74
  let qrRawUrl = "";
65
75
  const loginPromise = telegram.startQrLogin((dataUrl) => {
@@ -73,7 +83,7 @@ server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image.
73
83
  await new Promise((r) => setTimeout(r, 500));
74
84
  }
75
85
  if (!qrDataUrl) {
76
- return { content: [{ type: "text", text: "Failed to generate QR code" }] };
86
+ return fail(new Error("Failed to generate QR code"));
77
87
  }
78
88
  // Login continues in background
79
89
  loginPromise.then((result) => {
@@ -112,36 +122,44 @@ server.tool("telegram-login", "Login to Telegram via QR code. Returns QR image.
112
122
  ],
113
123
  };
114
124
  });
115
- server.tool("telegram-send-message", "Send a message to a Telegram chat", {
116
- chatId: z.string().describe("Chat ID or username (e.g. @username or numeric ID)"),
117
- text: z.string().describe("Message text"),
118
- replyTo: z.number().optional().describe("Message ID to reply to"),
119
- parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
120
- topicId: z.number().optional().describe("Forum topic ID to send message into (for groups with Topics enabled)"),
125
+ server.registerTool("telegram-send-message", {
126
+ description: "Send a message to a Telegram chat",
127
+ inputSchema: {
128
+ chatId: z.string().describe("Chat ID or username (e.g. @username or numeric ID)"),
129
+ text: z.string().describe("Message text"),
130
+ replyTo: z.number().optional().describe("Message ID to reply to"),
131
+ parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
132
+ topicId: z.number().optional().describe("Forum topic ID to send message into (for groups with Topics enabled)"),
133
+ },
134
+ annotations: WRITE,
121
135
  }, async ({ chatId, text, replyTo, parseMode, topicId }) => {
122
136
  const err = await requireConnection();
123
137
  if (err)
124
- return { content: [{ type: "text", text: err }] };
138
+ return fail(new Error(err));
125
139
  try {
126
140
  await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
127
141
  const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
128
- return { content: [{ type: "text", text: `Message sent to ${dest}` }] };
142
+ return ok(`Message sent to ${dest}`);
129
143
  }
130
144
  catch (e) {
131
- return { content: [{ type: "text", text: `Send error: ${e.message}` }] };
132
- }
133
- });
134
- server.tool("telegram-list-chats", "List Telegram chats", {
135
- limit: z.number().default(20).describe("Number of chats to return"),
136
- offsetDate: z.number().optional().describe("Unix timestamp offset for pagination"),
137
- filterType: z
138
- .enum(["private", "group", "channel", "contact_requests"])
139
- .optional()
140
- .describe("Filter by chat type. 'contact_requests' shows only private chats from non-contacts"),
145
+ return fail(e);
146
+ }
147
+ });
148
+ server.registerTool("telegram-list-chats", {
149
+ description: "List Telegram chats with unread counts, type indicators, and contact status",
150
+ inputSchema: {
151
+ limit: z.number().default(20).describe("Number of chats to return"),
152
+ offsetDate: z.number().optional().describe("Unix timestamp offset for pagination"),
153
+ filterType: z
154
+ .enum(["private", "group", "channel", "contact_requests"])
155
+ .optional()
156
+ .describe("Filter by chat type. 'contact_requests' shows only private chats from non-contacts"),
157
+ },
158
+ annotations: READ_ONLY,
141
159
  }, async ({ limit, offsetDate, filterType }) => {
142
160
  const err = await requireConnection();
143
161
  if (err)
144
- return { content: [{ type: "text", text: err }] };
162
+ return fail(new Error(err));
145
163
  try {
146
164
  const dialogs = await telegram.getDialogs(limit, offsetDate, filterType);
147
165
  const text = dialogs
@@ -153,98 +171,118 @@ server.tool("telegram-list-chats", "List Telegram chats", {
153
171
  return `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${unread}`;
154
172
  })
155
173
  .join("\n");
156
- return { content: [{ type: "text", text: text || "No chats" }] };
174
+ return ok(sanitize(text) || "No chats");
157
175
  }
158
176
  catch (e) {
159
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
160
- }
161
- });
162
- server.tool("telegram-read-messages", "Read recent messages from a Telegram chat", {
163
- chatId: z.string().describe("Chat ID or username"),
164
- limit: z.number().default(10).describe("Number of messages to return"),
165
- offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
166
- minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
167
- maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
177
+ return fail(e);
178
+ }
179
+ });
180
+ server.registerTool("telegram-read-messages", {
181
+ description: "Read recent messages from a Telegram chat with sender names, dates, media info, and reactions",
182
+ inputSchema: {
183
+ chatId: z.string().describe("Chat ID or username"),
184
+ limit: z.number().default(10).describe("Number of messages to return"),
185
+ offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
186
+ minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
187
+ maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
188
+ },
189
+ annotations: READ_ONLY,
168
190
  }, async ({ chatId, limit, offsetId, minDate, maxDate }) => {
169
191
  const err = await requireConnection();
170
192
  if (err)
171
- return { content: [{ type: "text", text: err }] };
193
+ return fail(new Error(err));
172
194
  try {
173
195
  const messages = await telegram.getMessages(chatId, limit, offsetId, minDate, maxDate);
174
196
  const text = messages
175
197
  .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
176
198
  .join("\n\n");
177
- return { content: [{ type: "text", text: text || "No messages" }] };
199
+ return ok(sanitize(text) || "No messages");
178
200
  }
179
201
  catch (e) {
180
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
202
+ return fail(e);
181
203
  }
182
204
  });
183
- server.tool("telegram-search-chats", "Search for Telegram chats/users/channels by name or username", {
184
- query: z.string().describe("Search query (name or username)"),
185
- limit: z.number().default(10).describe("Max results"),
205
+ server.registerTool("telegram-search-chats", {
206
+ description: "Search for Telegram chats, users, or channels by name or username. Returns description and member count",
207
+ inputSchema: {
208
+ query: z.string().describe("Search query (name or username)"),
209
+ limit: z.number().default(10).describe("Max results"),
210
+ },
211
+ annotations: READ_ONLY,
186
212
  }, async ({ query, limit }) => {
187
213
  const err = await requireConnection();
188
214
  if (err)
189
- return { content: [{ type: "text", text: err }] };
215
+ return fail(new Error(err));
190
216
  try {
191
217
  const results = await telegram.searchChats(query, limit);
192
218
  const text = results
193
219
  .map((c) => `${c.type === "group" ? "G" : c.type === "channel" ? "C" : "P"} ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})${c.membersCount ? ` [${c.membersCount} members]` : ""}${c.description ? ` — ${c.description.split("\n")[0].slice(0, 100)}` : ""}`)
194
220
  .join("\n");
195
- return { content: [{ type: "text", text: text || "No results" }] };
221
+ return ok(sanitize(text) || "No results");
196
222
  }
197
223
  catch (e) {
198
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
199
- }
200
- });
201
- server.tool("telegram-search-global", "Search messages globally across all public Telegram chats and channels", {
202
- query: z.string().describe("Search text"),
203
- limit: z.number().default(20).describe("Max results"),
204
- minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
205
- maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
224
+ return fail(e);
225
+ }
226
+ });
227
+ server.registerTool("telegram-search-global", {
228
+ description: "Search messages globally across all public Telegram chats and channels",
229
+ inputSchema: {
230
+ query: z.string().describe("Search text"),
231
+ limit: z.number().default(20).describe("Max results"),
232
+ minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
233
+ maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
234
+ },
235
+ annotations: READ_ONLY,
206
236
  }, async ({ query, limit, minDate, maxDate }) => {
207
237
  const err = await requireConnection();
208
238
  if (err)
209
- return { content: [{ type: "text", text: err }] };
239
+ return fail(new Error(err));
210
240
  try {
211
241
  const messages = await telegram.searchGlobal(query, limit, minDate, maxDate);
212
242
  const text = messages
213
243
  .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
214
244
  .join("\n\n");
215
- return { content: [{ type: "text", text: text || "No messages found" }] };
245
+ return ok(sanitize(text) || "No messages found");
216
246
  }
217
247
  catch (e) {
218
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
219
- }
220
- });
221
- server.tool("telegram-search-messages", "Search messages in a Telegram chat by text", {
222
- chatId: z.string().describe("Chat ID or username"),
223
- query: z.string().describe("Search text"),
224
- limit: z.number().default(20).describe("Max results"),
225
- minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
226
- maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
248
+ return fail(e);
249
+ }
250
+ });
251
+ server.registerTool("telegram-search-messages", {
252
+ description: "Search messages in a specific Telegram chat by text",
253
+ inputSchema: {
254
+ chatId: z.string().describe("Chat ID or username"),
255
+ query: z.string().describe("Search text"),
256
+ limit: z.number().default(20).describe("Max results"),
257
+ minDate: z.number().optional().describe("Unix timestamp: only messages after this date"),
258
+ maxDate: z.number().optional().describe("Unix timestamp: only messages before this date"),
259
+ },
260
+ annotations: READ_ONLY,
227
261
  }, async ({ chatId, query, limit, minDate, maxDate }) => {
228
262
  const err = await requireConnection();
229
263
  if (err)
230
- return { content: [{ type: "text", text: err }] };
264
+ return fail(new Error(err));
231
265
  try {
232
266
  const messages = await telegram.searchMessages(chatId, query, limit, minDate, maxDate);
233
267
  const text = messages
234
268
  .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
235
269
  .join("\n\n");
236
- return { content: [{ type: "text", text: text || "No messages found" }] };
270
+ return ok(sanitize(text) || "No messages found");
237
271
  }
238
272
  catch (e) {
239
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
273
+ return fail(e);
240
274
  }
241
275
  });
242
- server.tool("telegram-get-unread", "Get unread Telegram chats", {
243
- limit: z.number().default(20).describe("Number of unread chats to return"),
276
+ server.registerTool("telegram-get-unread", {
277
+ description: "Get chats with unread messages. Forums show per-topic unread breakdown",
278
+ inputSchema: {
279
+ limit: z.number().default(20).describe("Number of unread chats to return"),
280
+ },
281
+ annotations: READ_ONLY,
244
282
  }, async ({ limit }) => {
245
283
  const err = await requireConnection();
246
284
  if (err)
247
- return { content: [{ type: "text", text: err }] };
285
+ return fail(new Error(err));
248
286
  try {
249
287
  const dialogs = await telegram.getUnreadDialogs(limit);
250
288
  const text = dialogs
@@ -261,83 +299,95 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
261
299
  return line;
262
300
  })
263
301
  .join("\n");
264
- return { content: [{ type: "text", text: text || "No unread chats" }] };
302
+ return ok(sanitize(text) || "No unread chats");
265
303
  }
266
304
  catch (e) {
267
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
305
+ return fail(e);
268
306
  }
269
307
  });
270
- server.tool("telegram-mark-as-read", "Mark a Telegram chat as read", {
271
- chatId: z.string().describe("Chat ID or username"),
308
+ server.registerTool("telegram-mark-as-read", {
309
+ description: "Mark a Telegram chat as read",
310
+ inputSchema: { chatId: z.string().describe("Chat ID or username") },
311
+ annotations: WRITE,
272
312
  }, async ({ chatId }) => {
273
313
  const err = await requireConnection();
274
314
  if (err)
275
- return { content: [{ type: "text", text: err }] };
315
+ return fail(new Error(err));
276
316
  try {
277
317
  await telegram.markAsRead(chatId);
278
- return { content: [{ type: "text", text: `Marked ${chatId} as read` }] };
318
+ return ok(`Marked ${chatId} as read`);
279
319
  }
280
320
  catch (e) {
281
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
321
+ return fail(e);
282
322
  }
283
323
  });
284
- server.tool("telegram-forward-message", "Forward messages between Telegram chats", {
285
- fromChatId: z.string().describe("Source chat ID or username"),
286
- toChatId: z.string().describe("Destination chat ID or username"),
287
- messageIds: z.array(z.number()).describe("Array of message IDs to forward"),
324
+ server.registerTool("telegram-forward-message", {
325
+ description: "Forward messages between Telegram chats",
326
+ inputSchema: {
327
+ fromChatId: z.string().describe("Source chat ID or username"),
328
+ toChatId: z.string().describe("Destination chat ID or username"),
329
+ messageIds: z.array(z.number()).describe("Array of message IDs to forward"),
330
+ },
331
+ annotations: WRITE,
288
332
  }, async ({ fromChatId, toChatId, messageIds }) => {
289
333
  const err = await requireConnection();
290
334
  if (err)
291
- return { content: [{ type: "text", text: err }] };
335
+ return fail(new Error(err));
292
336
  try {
293
337
  await telegram.forwardMessage(fromChatId, toChatId, messageIds);
294
- return {
295
- content: [
296
- { type: "text", text: `Forwarded ${messageIds.length} message(s) from ${fromChatId} to ${toChatId}` },
297
- ],
298
- };
338
+ return ok(`Forwarded ${messageIds.length} message(s) from ${fromChatId} to ${toChatId}`);
299
339
  }
300
340
  catch (e) {
301
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
341
+ return fail(e);
302
342
  }
303
343
  });
304
- server.tool("telegram-edit-message", "Edit a sent message in Telegram", {
305
- chatId: z.string().describe("Chat ID or username"),
306
- messageId: z.number().describe("ID of the message to edit"),
307
- text: z.string().describe("New message text"),
344
+ server.registerTool("telegram-edit-message", {
345
+ description: "Edit a previously sent message in Telegram",
346
+ inputSchema: {
347
+ chatId: z.string().describe("Chat ID or username"),
348
+ messageId: z.number().describe("ID of the message to edit"),
349
+ text: z.string().describe("New message text"),
350
+ },
351
+ annotations: WRITE,
308
352
  }, async ({ chatId, messageId, text }) => {
309
353
  const err = await requireConnection();
310
354
  if (err)
311
- return { content: [{ type: "text", text: err }] };
355
+ return fail(new Error(err));
312
356
  try {
313
357
  await telegram.editMessage(chatId, messageId, text);
314
- return { content: [{ type: "text", text: `Message ${messageId} edited in ${chatId}` }] };
358
+ return ok(`Message ${messageId} edited in ${chatId}`);
315
359
  }
316
360
  catch (e) {
317
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
361
+ return fail(e);
318
362
  }
319
363
  });
320
- server.tool("telegram-delete-message", "Delete messages in a Telegram chat", {
321
- chatId: z.string().describe("Chat ID or username"),
322
- messageIds: z.array(z.number()).describe("Array of message IDs to delete"),
364
+ server.registerTool("telegram-delete-message", {
365
+ description: "Delete messages in a Telegram chat",
366
+ inputSchema: {
367
+ chatId: z.string().describe("Chat ID or username"),
368
+ messageIds: z.array(z.number()).describe("Array of message IDs to delete"),
369
+ },
370
+ annotations: DESTRUCTIVE,
323
371
  }, async ({ chatId, messageIds }) => {
324
372
  const err = await requireConnection();
325
373
  if (err)
326
- return { content: [{ type: "text", text: err }] };
374
+ return fail(new Error(err));
327
375
  try {
328
376
  await telegram.deleteMessages(chatId, messageIds);
329
- return { content: [{ type: "text", text: `Deleted ${messageIds.length} message(s) in ${chatId}` }] };
377
+ return ok(`Deleted ${messageIds.length} message(s) in ${chatId}`);
330
378
  }
331
379
  catch (e) {
332
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
380
+ return fail(e);
333
381
  }
334
382
  });
335
- server.tool("telegram-get-chat-info", "Get detailed info about a Telegram chat", {
336
- chatId: z.string().describe("Chat ID or username"),
383
+ server.registerTool("telegram-get-chat-info", {
384
+ description: "Get detailed info about a Telegram chat including name, type, members, description, and forum status",
385
+ inputSchema: { chatId: z.string().describe("Chat ID or username") },
386
+ annotations: READ_ONLY,
337
387
  }, async ({ chatId }) => {
338
388
  const err = await requireConnection();
339
389
  if (err)
340
- return { content: [{ type: "text", text: err }] };
390
+ return fail(new Error(err));
341
391
  try {
342
392
  const info = await telegram.getChatInfo(chatId);
343
393
  const lines = [
@@ -349,114 +399,138 @@ server.tool("telegram-get-chat-info", "Get detailed info about a Telegram chat",
349
399
  ...(info.description ? [`Description: ${info.description}`] : []),
350
400
  ...(info.membersCount != null ? [`Members: ${info.membersCount}`] : []),
351
401
  ];
352
- return { content: [{ type: "text", text: lines.join("\n") }] };
402
+ return ok(lines.join("\n"));
353
403
  }
354
404
  catch (e) {
355
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
405
+ return fail(e);
356
406
  }
357
407
  });
358
- server.tool("telegram-send-file", "Send a file to a Telegram chat", {
359
- chatId: z.string().describe("Chat ID or username"),
360
- filePath: z.string().describe("Absolute path to file"),
361
- caption: z.string().optional().describe("File caption"),
408
+ server.registerTool("telegram-send-file", {
409
+ description: "Send a file (photo, document, video, etc.) to a Telegram chat",
410
+ inputSchema: {
411
+ chatId: z.string().describe("Chat ID or username"),
412
+ filePath: z.string().describe("Absolute path to file"),
413
+ caption: z.string().optional().describe("File caption"),
414
+ },
415
+ annotations: WRITE,
362
416
  }, async ({ chatId, filePath, caption }) => {
363
417
  const err = await requireConnection();
364
418
  if (err)
365
- return { content: [{ type: "text", text: err }] };
419
+ return fail(new Error(err));
366
420
  try {
367
421
  await telegram.sendFile(chatId, filePath, caption);
368
- return { content: [{ type: "text", text: `File sent to ${chatId}` }] };
422
+ return ok(`File sent to ${chatId}`);
369
423
  }
370
424
  catch (e) {
371
- return { content: [{ type: "text", text: `Send file error: ${e.message}` }] };
425
+ return fail(e);
372
426
  }
373
427
  });
374
- server.tool("telegram-download-media", "Download media from a Telegram message", {
375
- chatId: z.string().describe("Chat ID or username"),
376
- messageId: z.number().describe("Message ID containing media"),
377
- downloadPath: z.string().describe("Absolute path to save file"),
428
+ server.registerTool("telegram-download-media", {
429
+ description: "Download media from a Telegram message to a local file",
430
+ inputSchema: {
431
+ chatId: z.string().describe("Chat ID or username"),
432
+ messageId: z.number().describe("Message ID containing media"),
433
+ downloadPath: z.string().describe("Absolute path to save file"),
434
+ },
435
+ annotations: READ_ONLY,
378
436
  }, async ({ chatId, messageId, downloadPath }) => {
379
437
  const err = await requireConnection();
380
438
  if (err)
381
- return { content: [{ type: "text", text: err }] };
439
+ return fail(new Error(err));
382
440
  try {
383
441
  const path = await telegram.downloadMedia(chatId, messageId, downloadPath);
384
- return { content: [{ type: "text", text: `Media downloaded to ${path}` }] };
442
+ return ok(`Media downloaded to ${path}`);
385
443
  }
386
444
  catch (e) {
387
- return { content: [{ type: "text", text: `Download error: ${e.message}` }] };
445
+ return fail(e);
388
446
  }
389
447
  });
390
- server.tool("telegram-pin-message", "Pin a message in a Telegram chat", {
391
- chatId: z.string().describe("Chat ID or username"),
392
- messageId: z.number().describe("Message ID to pin"),
393
- silent: z.boolean().default(false).describe("Pin without notification"),
448
+ server.registerTool("telegram-pin-message", {
449
+ description: "Pin a message in a Telegram chat",
450
+ inputSchema: {
451
+ chatId: z.string().describe("Chat ID or username"),
452
+ messageId: z.number().describe("Message ID to pin"),
453
+ silent: z.boolean().default(false).describe("Pin without notification"),
454
+ },
455
+ annotations: WRITE,
394
456
  }, async ({ chatId, messageId, silent }) => {
395
457
  const err = await requireConnection();
396
458
  if (err)
397
- return { content: [{ type: "text", text: err }] };
459
+ return fail(new Error(err));
398
460
  try {
399
461
  await telegram.pinMessage(chatId, messageId, silent);
400
- return { content: [{ type: "text", text: `Message ${messageId} pinned in ${chatId}` }] };
462
+ return ok(`Message ${messageId} pinned in ${chatId}`);
401
463
  }
402
464
  catch (e) {
403
- return { content: [{ type: "text", text: `Pin error: ${e.message}` }] };
465
+ return fail(e);
404
466
  }
405
467
  });
406
- server.tool("telegram-unpin-message", "Unpin a message in a Telegram chat", {
407
- chatId: z.string().describe("Chat ID or username"),
408
- messageId: z.number().describe("Message ID to unpin"),
468
+ server.registerTool("telegram-unpin-message", {
469
+ description: "Unpin a message in a Telegram chat",
470
+ inputSchema: {
471
+ chatId: z.string().describe("Chat ID or username"),
472
+ messageId: z.number().describe("Message ID to unpin"),
473
+ },
474
+ annotations: WRITE,
409
475
  }, async ({ chatId, messageId }) => {
410
476
  const err = await requireConnection();
411
477
  if (err)
412
- return { content: [{ type: "text", text: err }] };
478
+ return fail(new Error(err));
413
479
  try {
414
480
  await telegram.unpinMessage(chatId, messageId);
415
- return { content: [{ type: "text", text: `Message ${messageId} unpinned in ${chatId}` }] };
481
+ return ok(`Message ${messageId} unpinned in ${chatId}`);
416
482
  }
417
483
  catch (e) {
418
- return { content: [{ type: "text", text: `Unpin error: ${e.message}` }] };
484
+ return fail(e);
419
485
  }
420
486
  });
421
- server.tool("telegram-get-contacts", "Get Telegram contacts list", {
422
- limit: z.number().default(50).describe("Number of contacts to return"),
487
+ server.registerTool("telegram-get-contacts", {
488
+ description: "Get your Telegram contacts list with phone numbers",
489
+ inputSchema: { limit: z.number().default(50).describe("Number of contacts to return") },
490
+ annotations: READ_ONLY,
423
491
  }, async ({ limit }) => {
424
492
  const err = await requireConnection();
425
493
  if (err)
426
- return { content: [{ type: "text", text: err }] };
494
+ return fail(new Error(err));
427
495
  try {
428
496
  const contacts = await telegram.getContacts(limit);
429
497
  const text = contacts
430
498
  .map((c) => `P ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})${c.phone ? ` +${c.phone}` : ""}`)
431
499
  .join("\n");
432
- return { content: [{ type: "text", text: text || "No contacts" }] };
500
+ return ok(sanitize(text) || "No contacts");
433
501
  }
434
502
  catch (e) {
435
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
503
+ return fail(e);
436
504
  }
437
505
  });
438
- server.tool("telegram-get-chat-members", "Get members of a Telegram group or channel", {
439
- chatId: z.string().describe("Chat ID or username"),
440
- limit: z.number().default(50).describe("Number of members to return"),
506
+ server.registerTool("telegram-get-chat-members", {
507
+ description: "Get members of a Telegram group or channel",
508
+ inputSchema: {
509
+ chatId: z.string().describe("Chat ID or username"),
510
+ limit: z.number().default(50).describe("Number of members to return"),
511
+ },
512
+ annotations: READ_ONLY,
441
513
  }, async ({ chatId, limit }) => {
442
514
  const err = await requireConnection();
443
515
  if (err)
444
- return { content: [{ type: "text", text: err }] };
516
+ return fail(new Error(err));
445
517
  try {
446
518
  const members = await telegram.getChatMembers(chatId, limit);
447
519
  const text = members.map((m) => `${m.name}${m.username ? ` (@${m.username})` : ""} (${m.id})`).join("\n");
448
- return { content: [{ type: "text", text: text || "No members found" }] };
520
+ return ok(sanitize(text) || "No members found");
449
521
  }
450
522
  catch (e) {
451
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
523
+ return fail(e);
452
524
  }
453
525
  });
454
- server.tool("telegram-get-profile", "Get detailed profile info of a Telegram user including bio, birthday, business info and more", {
455
- userId: z.string().describe("User ID or username"),
526
+ server.registerTool("telegram-get-profile", {
527
+ description: "Get detailed profile info of a Telegram user including bio, birthday, premium status, business info and more",
528
+ inputSchema: { userId: z.string().describe("User ID or username") },
529
+ annotations: READ_ONLY,
456
530
  }, async ({ userId }) => {
457
531
  const err = await requireConnection();
458
532
  if (err)
459
- return { content: [{ type: "text", text: err }] };
533
+ return fail(new Error(err));
460
534
  try {
461
535
  const profile = await telegram.getProfile(userId);
462
536
  const lines = [
@@ -474,83 +548,87 @@ server.tool("telegram-get-profile", "Get detailed profile info of a Telegram use
474
548
  ...(profile.businessLocation ? [`Business location: ${profile.businessLocation}`] : []),
475
549
  ...(profile.businessWorkHours ? [`Business hours timezone: ${profile.businessWorkHours}`] : []),
476
550
  ];
477
- return { content: [{ type: "text", text: lines.join("\n") }] };
551
+ return ok(lines.join("\n"));
478
552
  }
479
553
  catch (e) {
480
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
481
- }
482
- });
483
- server.tool("telegram-get-profile-photo", "Download profile photo of a Telegram user, group, or channel. Returns inline image or saves to file", {
484
- entityId: z.string().describe("User/Chat/Channel ID or username"),
485
- savePath: z.string().optional().describe("Absolute path to save file. If omitted, returns inline base64 image"),
486
- size: z
487
- .enum(["small", "big"])
488
- .optional()
489
- .describe("Photo size: 'small' (160x160) or 'big' (640x640). Default: big"),
554
+ return fail(e);
555
+ }
556
+ });
557
+ server.registerTool("telegram-get-profile-photo", {
558
+ description: "Download profile photo of a Telegram user, group, or channel. Returns inline image or saves to file",
559
+ inputSchema: {
560
+ entityId: z.string().describe("User/Chat/Channel ID or username"),
561
+ savePath: z.string().optional().describe("Absolute path to save file. If omitted, returns inline base64 image"),
562
+ size: z
563
+ .enum(["small", "big"])
564
+ .optional()
565
+ .describe("Photo size: 'small' (160x160) or 'big' (640x640). Default: big"),
566
+ },
567
+ annotations: READ_ONLY,
490
568
  }, async ({ entityId, savePath, size }) => {
491
569
  const err = await requireConnection();
492
570
  if (err)
493
- return { content: [{ type: "text", text: err }] };
571
+ return fail(new Error(err));
494
572
  try {
495
573
  const result = await telegram.downloadProfilePhoto(entityId, {
496
574
  isBig: size !== "small",
497
575
  savePath,
498
576
  });
499
577
  if (!result) {
500
- return { content: [{ type: "text", text: "No profile photo found" }] };
578
+ return ok("No profile photo found");
501
579
  }
502
580
  if ("filePath" in result) {
503
- return { content: [{ type: "text", text: `Downloaded to: ${result.filePath}` }] };
581
+ return ok(`Downloaded to: ${result.filePath}`);
504
582
  }
505
583
  return {
506
584
  content: [
507
585
  { type: "image", data: result.buffer.toString("base64"), mimeType: result.mimeType },
508
- { type: "text", text: `Profile photo (${(result.buffer.length / 1024).toFixed(0)} KB, ${result.mimeType})` },
586
+ {
587
+ type: "text",
588
+ text: `Profile photo (${(result.buffer.length / 1024).toFixed(0)} KB, ${result.mimeType})`,
589
+ },
509
590
  ],
510
591
  };
511
592
  }
512
593
  catch (e) {
513
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
594
+ return fail(e);
514
595
  }
515
596
  });
516
- server.tool("telegram-join-chat", "Join a Telegram group or channel by username or invite link", {
517
- target: z.string().describe("Username (@group), link (t.me/group), or invite link (t.me/+xxx)"),
597
+ server.registerTool("telegram-join-chat", {
598
+ description: "Join a Telegram group or channel by username or invite link",
599
+ inputSchema: { target: z.string().describe("Username (@group), link (t.me/group), or invite link (t.me/+xxx)") },
600
+ annotations: WRITE,
518
601
  }, async ({ target }) => {
519
602
  const err = await requireConnection();
520
603
  if (err)
521
- return { content: [{ type: "text", text: err }] };
604
+ return fail(new Error(err));
522
605
  try {
523
606
  const result = await telegram.joinChat(target);
524
- return {
525
- content: [
526
- {
527
- type: "text",
528
- text: `Joined ${result.type}: ${result.title} (ID: ${result.id})`,
529
- },
530
- ],
531
- };
607
+ return ok(`Joined ${result.type}: ${result.title} (ID: ${result.id})`);
532
608
  }
533
609
  catch (e) {
534
- return {
535
- content: [{ type: "text", text: `Error: ${e.message}` }],
536
- };
537
- }
538
- });
539
- server.tool("telegram-send-reaction", "Send emoji reaction(s) to a message. Supports multiple reactions and adding to existing ones. Omit emoji to remove all reactions", {
540
- chatId: z.string().describe("Chat ID or username"),
541
- messageId: z.number().describe("Message ID to react to"),
542
- emoji: z
543
- .union([z.string(), z.array(z.string())])
544
- .optional()
545
- .describe("Reaction emoji(s): single '👍' or array ['👍','🔥']. Omit to remove all reactions"),
546
- addToExisting: z
547
- .boolean()
548
- .default(false)
549
- .describe("If true, add reaction(s) to existing ones instead of replacing"),
610
+ return fail(e);
611
+ }
612
+ });
613
+ server.registerTool("telegram-send-reaction", {
614
+ description: "Send emoji reaction(s) to a message. Supports multiple reactions and adding to existing ones. Omit emoji to remove all reactions",
615
+ inputSchema: {
616
+ chatId: z.string().describe("Chat ID or username"),
617
+ messageId: z.number().describe("Message ID to react to"),
618
+ emoji: z
619
+ .union([z.string(), z.array(z.string())])
620
+ .optional()
621
+ .describe("Reaction emoji(s): single '👍' or array ['👍','🔥']. Omit to remove all reactions"),
622
+ addToExisting: z
623
+ .boolean()
624
+ .default(false)
625
+ .describe("If true, add reaction(s) to existing ones instead of replacing"),
626
+ },
627
+ annotations: WRITE,
550
628
  }, async ({ chatId, messageId, emoji, addToExisting }) => {
551
629
  const err = await requireConnection();
552
630
  if (err)
553
- return { content: [{ type: "text", text: err }] };
631
+ return fail(new Error(err));
554
632
  try {
555
633
  const updated = await telegram.sendReaction(chatId, messageId, emoji, addToExisting);
556
634
  const emojiStr = Array.isArray(emoji) ? emoji.join("") : emoji;
@@ -558,45 +636,53 @@ server.tool("telegram-send-reaction", "Send emoji reaction(s) to a message. Supp
558
636
  const reactionsInfo = updated
559
637
  ? ` | Reactions: ${updated.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`).join(" ")}`
560
638
  : "";
561
- return { content: [{ type: "text", text: `${action} message ${messageId} in ${chatId}${reactionsInfo}` }] };
639
+ return ok(`${action} message ${messageId} in ${chatId}${reactionsInfo}`);
562
640
  }
563
641
  catch (e) {
564
- return { content: [{ type: "text", text: `Reaction error: ${e.message}` }] };
642
+ return fail(e);
565
643
  }
566
644
  });
567
- server.tool("telegram-get-reactions", "Get detailed reaction info for a message: which reactions, counts, and who reacted (when visible)", {
568
- chatId: z.string().describe("Chat ID or username"),
569
- messageId: z.number().describe("Message ID to get reactions for"),
645
+ server.registerTool("telegram-get-reactions", {
646
+ description: "Get detailed reaction info for a message: which reactions, counts, and who reacted (when visible)",
647
+ inputSchema: {
648
+ chatId: z.string().describe("Chat ID or username"),
649
+ messageId: z.number().describe("Message ID to get reactions for"),
650
+ },
651
+ annotations: READ_ONLY,
570
652
  }, async ({ chatId, messageId }) => {
571
653
  const err = await requireConnection();
572
654
  if (err)
573
- return { content: [{ type: "text", text: err }] };
655
+ return fail(new Error(err));
574
656
  try {
575
657
  const result = await telegram.getMessageReactions(chatId, messageId);
576
658
  if (result.reactions.length === 0) {
577
- return { content: [{ type: "text", text: `No reactions on message ${messageId}` }] };
659
+ return ok(`No reactions on message ${messageId}`);
578
660
  }
579
661
  const lines = result.reactions.map((r) => {
580
662
  const usersStr = r.users.length > 0 ? `: ${r.users.map((u) => u.name).join(", ")}` : "";
581
663
  return `${r.emoji} × ${r.count}${usersStr}`;
582
664
  });
583
665
  lines.push(`\nTotal: ${result.total} reactions`);
584
- return { content: [{ type: "text", text: lines.join("\n") }] };
666
+ return ok(lines.join("\n"));
585
667
  }
586
668
  catch (e) {
587
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
588
- }
589
- });
590
- server.tool("telegram-send-scheduled", "Send a scheduled message to a Telegram chat. The message will be delivered at the specified time by Telegram servers", {
591
- chatId: z.string().describe("Chat ID or username (use 'me' or 'self' for Saved Messages)"),
592
- text: z.string().describe("Message text"),
593
- scheduleDate: z.number().describe("Unix timestamp when to send the message (must be in the future)"),
594
- replyTo: z.number().optional().describe("Message ID to reply to"),
595
- parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
669
+ return fail(e);
670
+ }
671
+ });
672
+ server.registerTool("telegram-send-scheduled", {
673
+ description: "Send a scheduled message to a Telegram chat. The message will be delivered at the specified time by Telegram servers",
674
+ inputSchema: {
675
+ chatId: z.string().describe("Chat ID or username (use 'me' or 'self' for Saved Messages)"),
676
+ text: z.string().describe("Message text"),
677
+ scheduleDate: z.number().describe("Unix timestamp when to send the message (must be in the future)"),
678
+ replyTo: z.number().optional().describe("Message ID to reply to"),
679
+ parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
680
+ },
681
+ annotations: WRITE,
596
682
  }, async ({ chatId, text, scheduleDate, replyTo, parseMode }) => {
597
683
  const err = await requireConnection();
598
684
  if (err)
599
- return { content: [{ type: "text", text: err }] };
685
+ return fail(new Error(err));
600
686
  // Resolve 'me'/'self' to Saved Messages
601
687
  let target = chatId;
602
688
  if (target === "me" || target === "self") {
@@ -605,47 +691,53 @@ server.tool("telegram-send-scheduled", "Send a scheduled message to a Telegram c
605
691
  target = me.id;
606
692
  }
607
693
  catch {
608
- return { content: [{ type: "text", text: "Failed to resolve Saved Messages" }] };
694
+ return fail(new Error("Failed to resolve Saved Messages"));
609
695
  }
610
696
  }
611
697
  try {
612
698
  await telegram.sendScheduledMessage(target, text, scheduleDate, replyTo, parseMode);
613
699
  const date = new Date(scheduleDate * 1000).toISOString();
614
- return { content: [{ type: "text", text: `Message scheduled for ${date} in ${chatId}` }] };
700
+ return ok(`Message scheduled for ${date} in ${chatId}`);
615
701
  }
616
702
  catch (e) {
617
- return { content: [{ type: "text", text: `Schedule error: ${e.message}` }] };
618
- }
619
- });
620
- server.tool("telegram-create-poll", "Create a poll in a Telegram chat", {
621
- chatId: z.string().describe("Chat ID or username"),
622
- question: z.string().describe("Poll question"),
623
- answers: z.array(z.string()).min(2).max(10).describe("Answer options (2-10)"),
624
- multipleChoice: z.boolean().default(false).describe("Allow multiple answers"),
625
- quiz: z.boolean().default(false).describe("Quiz mode (one correct answer)"),
626
- correctAnswer: z.number().optional().describe("Index of correct answer (0-based, required for quiz mode)"),
703
+ return fail(e);
704
+ }
705
+ });
706
+ server.registerTool("telegram-create-poll", {
707
+ description: "Create a poll in a Telegram chat (multiple choice or quiz mode)",
708
+ inputSchema: {
709
+ chatId: z.string().describe("Chat ID or username"),
710
+ question: z.string().describe("Poll question"),
711
+ answers: z.array(z.string()).min(2).max(10).describe("Answer options (2-10)"),
712
+ multipleChoice: z.boolean().default(false).describe("Allow multiple answers"),
713
+ quiz: z.boolean().default(false).describe("Quiz mode (one correct answer)"),
714
+ correctAnswer: z.number().optional().describe("Index of correct answer (0-based, required for quiz mode)"),
715
+ },
716
+ annotations: WRITE,
627
717
  }, async ({ chatId, question, answers, multipleChoice, quiz, correctAnswer }) => {
628
718
  const err = await requireConnection();
629
719
  if (err)
630
- return { content: [{ type: "text", text: err }] };
720
+ return fail(new Error(err));
631
721
  try {
632
722
  const msgId = await telegram.createPoll(chatId, question, answers, { multipleChoice, quiz, correctAnswer });
633
- return { content: [{ type: "text", text: `Poll created in ${chatId}${msgId ? ` (message #${msgId})` : ""}` }] };
723
+ return ok(`Poll created in ${chatId}${msgId ? ` (message #${msgId})` : ""}`);
634
724
  }
635
725
  catch (e) {
636
- return { content: [{ type: "text", text: `Poll error: ${e.message}` }] };
726
+ return fail(e);
637
727
  }
638
728
  });
639
- server.tool("telegram-get-contact-requests", "Get incoming messages from non-contacts (contact requests). Shows who messaged you without being in your contacts, with message preview", {
640
- limit: z.number().default(20).describe("Number of contact requests to return"),
729
+ server.registerTool("telegram-get-contact-requests", {
730
+ description: "Get incoming messages from non-contacts (contact requests). Shows who messaged you without being in your contacts, with message preview",
731
+ inputSchema: { limit: z.number().default(20).describe("Number of contact requests to return") },
732
+ annotations: READ_ONLY,
641
733
  }, async ({ limit }) => {
642
734
  const err = await requireConnection();
643
735
  if (err)
644
- return { content: [{ type: "text", text: err }] };
736
+ return fail(new Error(err));
645
737
  try {
646
738
  const requests = await telegram.getContactRequests(limit);
647
739
  if (requests.length === 0) {
648
- return { content: [{ type: "text", text: "No contact requests" }] };
740
+ return ok("No contact requests");
649
741
  }
650
742
  const text = requests
651
743
  .map((r) => {
@@ -656,66 +748,76 @@ server.tool("telegram-get-contact-requests", "Get incoming messages from non-con
656
748
  return `${tag} ${r.name}${username} (${r.id})${unread}${preview}`;
657
749
  })
658
750
  .join("\n");
659
- return { content: [{ type: "text", text: text }] };
751
+ return ok(sanitize(text));
660
752
  }
661
753
  catch (e) {
662
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
663
- }
664
- });
665
- server.tool("telegram-add-contact", "Add a user to your Telegram contacts. Use this to accept contact requests from non-contacts", {
666
- userId: z.string().describe("User ID or username to add"),
667
- firstName: z.string().describe("First name for the contact"),
668
- lastName: z.string().optional().describe("Last name for the contact"),
669
- phone: z.string().optional().describe("Phone number for the contact"),
754
+ return fail(e);
755
+ }
756
+ });
757
+ server.registerTool("telegram-add-contact", {
758
+ description: "Add a user to your Telegram contacts. Use this to accept contact requests from non-contacts",
759
+ inputSchema: {
760
+ userId: z.string().describe("User ID or username to add"),
761
+ firstName: z.string().describe("First name for the contact"),
762
+ lastName: z.string().optional().describe("Last name for the contact"),
763
+ phone: z.string().optional().describe("Phone number for the contact"),
764
+ },
765
+ annotations: WRITE,
670
766
  }, async ({ userId, firstName, lastName, phone }) => {
671
767
  const err = await requireConnection();
672
768
  if (err)
673
- return { content: [{ type: "text", text: err }] };
769
+ return fail(new Error(err));
674
770
  try {
675
771
  await telegram.addContact(userId, firstName, lastName, phone);
676
- return {
677
- content: [{ type: "text", text: `Contact added: ${firstName}${lastName ? ` ${lastName}` : ""} (${userId})` }],
678
- };
772
+ return ok(`Contact added: ${firstName}${lastName ? ` ${lastName}` : ""} (${userId})`);
679
773
  }
680
774
  catch (e) {
681
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
775
+ return fail(e);
682
776
  }
683
777
  });
684
- server.tool("telegram-block-user", "Block a Telegram user. Blocked users cannot send you messages", {
685
- userId: z.string().describe("User ID or username to block"),
778
+ server.registerTool("telegram-block-user", {
779
+ description: "Block a Telegram user. Blocked users cannot send you messages",
780
+ inputSchema: { userId: z.string().describe("User ID or username to block") },
781
+ annotations: WRITE,
686
782
  }, async ({ userId }) => {
687
783
  const err = await requireConnection();
688
784
  if (err)
689
- return { content: [{ type: "text", text: err }] };
785
+ return fail(new Error(err));
690
786
  try {
691
787
  await telegram.blockUser(userId);
692
- return { content: [{ type: "text", text: `User blocked: ${userId}` }] };
788
+ return ok(`User blocked: ${userId}`);
693
789
  }
694
790
  catch (e) {
695
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
791
+ return fail(e);
696
792
  }
697
793
  });
698
- server.tool("telegram-report-spam", "Report a chat as spam to Telegram", {
699
- chatId: z.string().describe("Chat ID or username to report"),
794
+ server.registerTool("telegram-report-spam", {
795
+ description: "Report a chat as spam to Telegram",
796
+ inputSchema: { chatId: z.string().describe("Chat ID or username to report") },
797
+ annotations: WRITE,
700
798
  }, async ({ chatId }) => {
701
799
  const err = await requireConnection();
702
800
  if (err)
703
- return { content: [{ type: "text", text: err }] };
801
+ return fail(new Error(err));
704
802
  try {
705
803
  await telegram.reportSpam(chatId);
706
- return { content: [{ type: "text", text: `Reported as spam: ${chatId}` }] };
804
+ return ok(`Reported as spam: ${chatId}`);
707
805
  }
708
806
  catch (e) {
709
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
807
+ return fail(e);
710
808
  }
711
809
  });
712
- server.tool("telegram-list-topics", "List forum topics in a Telegram group with Topics enabled. Shows topic names, unread counts, and status", {
713
- chatId: z.string().describe("Chat ID or username of a group with Topics enabled"),
714
- limit: z.number().default(100).describe("Max topics to return"),
810
+ server.registerTool("telegram-list-topics", {
811
+ description: "List forum topics in a Telegram group with Topics enabled. Shows topic names, unread counts, and status",
812
+ inputSchema: {
813
+ chatId: z.string().describe("Chat ID or username of a group with Topics enabled"),
814
+ limit: z.number().default(100).describe("Max topics to return"),
815
+ },
816
+ annotations: READ_ONLY,
715
817
  }, async ({ chatId, limit }) => {
716
818
  const err = await requireConnection();
717
819
  if (err)
718
- return { content: [{ type: "text", text: err }] };
820
+ return fail(new Error(err));
719
821
  try {
720
822
  const topics = await telegram.getForumTopics(chatId, limit);
721
823
  const text = topics
@@ -726,30 +828,34 @@ server.tool("telegram-list-topics", "List forum topics in a Telegram group with
726
828
  return `# ${t.title} (id: ${t.id})${flagStr}${unread}`;
727
829
  })
728
830
  .join("\n");
729
- return { content: [{ type: "text", text: text || "No topics found" }] };
831
+ return ok(sanitize(text) || "No topics found");
730
832
  }
731
833
  catch (e) {
732
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
733
- }
734
- });
735
- server.tool("telegram-read-topic-messages", "Read messages from a specific forum topic in a Telegram group", {
736
- chatId: z.string().describe("Chat ID or username"),
737
- topicId: z.number().describe("Topic ID (get from telegram-list-topics)"),
738
- limit: z.number().default(20).describe("Number of messages to return"),
739
- offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
834
+ return fail(e);
835
+ }
836
+ });
837
+ server.registerTool("telegram-read-topic-messages", {
838
+ description: "Read messages from a specific forum topic in a Telegram group",
839
+ inputSchema: {
840
+ chatId: z.string().describe("Chat ID or username"),
841
+ topicId: z.number().describe("Topic ID (get from telegram-list-topics)"),
842
+ limit: z.number().default(20).describe("Number of messages to return"),
843
+ offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
844
+ },
845
+ annotations: READ_ONLY,
740
846
  }, async ({ chatId, topicId, limit, offsetId }) => {
741
847
  const err = await requireConnection();
742
848
  if (err)
743
- return { content: [{ type: "text", text: err }] };
849
+ return fail(new Error(err));
744
850
  try {
745
851
  const messages = await telegram.getTopicMessages(chatId, topicId, limit, offsetId);
746
852
  const text = messages
747
853
  .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
748
854
  .join("\n\n");
749
- return { content: [{ type: "text", text: text || "No messages in this topic" }] };
855
+ return ok(sanitize(text) || "No messages in this topic");
750
856
  }
751
857
  catch (e) {
752
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
858
+ return fail(e);
753
859
  }
754
860
  });
755
861
  // --- Start ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "url": "https://github.com/overpod/mcp-telegram/issues"
50
50
  },
51
51
  "dependencies": {
52
- "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "@modelcontextprotocol/sdk": "^1.28.0",
53
53
  "dotenv": "^17.3.1",
54
54
  "qrcode": "^1.5.4",
55
55
  "telegram": "^2.26.22",
@@ -60,6 +60,6 @@
60
60
  "@types/node": "^25.5.0",
61
61
  "@types/qrcode": "^1.5.6",
62
62
  "tsx": "^4.21.0",
63
- "typescript": "^5.9.3"
63
+ "typescript": "^6.0.2"
64
64
  }
65
65
  }