telegram-mcp-local-server
Version:
Secure Model Context Protocol (MCP) server for Telegram integration. Runs locally, allows AI agents to read chats and message history, with built-in readonly mode for safety.
249 lines • 10.1 kB
JavaScript
import { TelegramClient as TgClient } from "telegram";
import { StringSession } from "telegram/sessions/index.js";
import { Api } from "telegram";
export class TelegramClient {
constructor(config) {
this.config = config;
const session = new StringSession(config.sessionString || "");
this.client = new TgClient(session, config.apiId, config.apiHash, {
connectionRetries: 5,
});
}
async connect() {
//console.error("Connecting to Telegram...");
await this.client.connect();
//console.error("Connected to Telegram successfully");
}
async getChats(limit = 50) {
//console.error(`Fetching ${limit} chats...`);
const dialogs = await this.client.getDialogs({ limit });
const chats = [];
for (const dialog of dialogs) {
const entity = dialog.entity;
if (!entity)
continue;
let chatInfo;
if (entity.className === "User") {
chatInfo = {
id: entity.id.toString(),
title: `${entity.firstName || ""} ${entity.lastName || ""}`.trim(),
type: "user",
username: entity.username,
};
}
else if (entity.className === "Chat") {
chatInfo = {
id: entity.id.toString(),
title: entity.title,
type: "group",
participantsCount: entity.participantsCount,
};
}
else if (entity.className === "Channel") {
chatInfo = {
id: entity.id.toString(),
title: entity.title,
type: entity.broadcast ? "channel" : "supergroup",
username: entity.username,
participantsCount: entity.participantsCount,
};
}
else {
continue; // Skip unknown entity types
}
chats.push(chatInfo);
}
//console.error(`Fetched ${chats.length} chats`);
return chats;
}
async getChatHistory(chatId, limit = 50, offsetId) {
//console.error(`Fetching ${limit} messages from chat ${chatId}...`);
try {
const entity = await this.client.getEntity(chatId);
const messages = await this.client.getMessages(entity, {
limit,
offsetId,
});
const messageInfos = [];
for (const message of messages) {
const msg = message;
if (!msg || msg.className !== "Message") {
continue;
}
let fromUsername;
let fromFirstName;
let fromLastName;
if (msg.fromId) {
try {
const sender = await this.client.getEntity(msg.fromId);
if (sender?.className === "User") {
fromUsername = sender.username;
fromFirstName = sender.firstName;
fromLastName = sender.lastName;
}
}
catch (error) {
console.error("Error getting sender info:", error);
}
}
const messageInfo = {
id: msg.id,
text: msg.message || "",
date: new Date(msg.date * 1000),
fromId: msg.fromId?.toString(),
fromUsername,
fromFirstName,
fromLastName,
replyToMsgId: msg.replyTo?.replyToMsgId,
};
messageInfos.push(messageInfo);
}
// console.error(`Fetched ${messageInfos.length} messages`);
return messageInfos;
}
catch (error) {
// console.error(`Error fetching chat history for ${chatId}:`, error);
throw error;
}
}
async sendMessage(chatId, message) {
// console.error(`Sending message to chat ${chatId}...`);
try {
const entity = await this.client.getEntity(chatId);
const result = await this.client.sendMessage(entity, { message });
if (!result || result.className !== "Message") {
throw new Error("Failed to send message");
}
const messageInfo = {
id: result.id,
text: result.message || "",
date: new Date(result.date * 1000),
fromId: result.fromId?.toString(),
};
// console.error("Message sent successfully");
return messageInfo;
}
catch (error) {
// console.error(`Error sending message to ${chatId}:`, error);
throw error;
}
}
async getFolders() {
// console.error("Fetching dialog folders...");
try {
const filters = await this.client.invoke(new Api.messages.GetDialogFilters());
const folders = [];
for (const f of filters) {
if (f && (f instanceof Api.DialogFilter)) {
const titleVal = typeof f.title === 'string'
? f.title
: (f.title?.text ?? '');
folders.push({ id: Number(f.id), title: String(titleVal), emoticon: f.emoticon });
}
// Skip DialogFilterDefault and other types
}
// console.error(`Fetched ${folders.length} folders`);
return folders;
}
catch (error) {
// console.error("Error fetching dialog folders:", error);
throw error;
}
}
async getChannelsFromFolder(folderId, limit = 50) {
// console.error(`Fetching up to ${limit} channels from folder ${folderId}...`);
try {
const filters = await this.client.invoke(new Api.messages.GetDialogFilters());
let targetFilter;
for (const f of filters) {
if (f && (f instanceof Api.DialogFilter) && Number(f.id) === Number(folderId)) {
targetFilter = f;
break;
}
}
if (!targetFilter) {
throw new Error(`Folder with id ${folderId} not found`);
}
const toPeerKey = (p) => {
if (!p)
return null;
// Handle InputPeer and Peer variants by checking known id fields
if (typeof p.channelId !== 'undefined')
return `channel:${p.channelId.toString()}`;
if (typeof p.chatId !== 'undefined')
return `chat:${p.chatId.toString()}`;
if (typeof p.userId !== 'undefined')
return `user:${p.userId.toString()}`;
return null;
};
const included = new Set();
const excluded = new Set();
const includePeers = (targetFilter.includePeers || []);
const excludePeers = (targetFilter.excludePeers || []);
const pinnedPeers = (targetFilter.pinnedPeers || []);
for (const p of [...includePeers, ...pinnedPeers]) {
const k = toPeerKey(p);
if (k)
included.add(k);
}
for (const p of excludePeers) {
const k = toPeerKey(p);
if (k)
excluded.add(k);
}
// Get a reasonably large set of dialogs to filter from
const dialogs = await this.client.getDialogs({ limit: Math.max(limit * 4, 200) });
const channels = [];
for (const dialog of dialogs) {
const entity = dialog.entity;
if (!entity)
continue;
const entityKey = entity.className === 'Channel'
? `channel:${entity.id.toString()}`
: entity.className === 'Chat'
? `chat:${entity.id.toString()}`
: entity.className === 'User'
? `user:${entity.id.toString()}`
: null;
if (!entityKey)
continue;
// Determine inclusion according to filter
let inFilter = true;
if (included.size > 0) {
inFilter = included.has(entityKey);
}
else {
if (excluded.has(entityKey))
inFilter = false;
// Note: For simplicity, we don't evaluate category flags (contacts, groups, channels, etc.) here.
}
if (!inFilter)
continue;
// Only return channels/supergroups
if (entity.className === 'Channel') {
channels.push({
id: entity.id.toString(),
title: entity.title,
type: entity.broadcast ? 'channel' : 'supergroup',
username: entity.username,
participantsCount: entity.participantsCount,
});
}
if (channels.length >= limit)
break;
}
// console.error(`Fetched ${channels.length} channels from folder ${folderId}`);
return channels;
}
catch (error) {
// console.error(`Error fetching channels from folder ${folderId}:`, error);
throw error;
}
}
async disconnect() {
// console.error("Disconnecting from Telegram...");
await this.client.disconnect();
// console.error("Disconnected from Telegram");
}
}
//# sourceMappingURL=telegram-client.js.map