import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import TelegramBot from 'node-telegram-bot-api';
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cors());
// Get environment variables with fallback defaults for Railway
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '8198346055:AAG01qXWGBwP4qzDlkZztPwshDdYw_DLFN0';
const CHANNEL_ID = process.env.TELEGRAM_CHANNEL_ID || '@mymcptest';
const PEXELS_API_KEY = process.env.PEXELS_API_KEY || 'j0CGX7JRiZOmEz1bEYvxZeRn1cS7qdFy5351PmDtZ01Wnby18AIeWt32';
const YANDEX_CLIENT_ID = process.env.YANDEX_CLIENT_ID || '11221f6ebd2d47649d42d9f4b282a876';
const YANDEX_CLIENT_SECRET = process.env.YANDEX_CLIENT_SECRET || 'eb793370893544d683bf277d14bfd842';
const YANDEX_LOGIN = process.env.YANDEX_LOGIN || 'bobi-dk91';
let YANDEX_OAUTH_TOKEN = process.env.YANDEX_OAUTH_TOKEN || '';
// Log environment status
if (process.env.TELEGRAM_BOT_TOKEN) {
console.log('π§ Using TELEGRAM_BOT_TOKEN from environment variables');
} else {
console.log('β οΈ Using default TELEGRAM_BOT_TOKEN (consider setting environment variable)');
}
if (process.env.TELEGRAM_CHANNEL_ID) {
console.log('π§ Using TELEGRAM_CHANNEL_ID from environment variables');
} else {
console.log('β οΈ Using default TELEGRAM_CHANNEL_ID (consider setting environment variable)');
}
console.log('π§ Environment variables:');
console.log('TELEGRAM_BOT_TOKEN:', TELEGRAM_BOT_TOKEN ? 'SET' : 'NOT SET');
console.log('TELEGRAM_CHANNEL_ID:', CHANNEL_ID);
console.log('PEXELS_API_KEY:', PEXELS_API_KEY ? 'SET' : 'NOT SET');
console.log('YANDEX_CLIENT_ID:', YANDEX_CLIENT_ID ? 'SET' : 'NOT SET');
console.log('YANDEX_OAUTH_TOKEN:', YANDEX_OAUTH_TOKEN ? 'SET' : 'NOT SET');
// Initialize Telegram Bot
let bot;
try {
bot = new TelegramBot(TELEGRAM_BOT_TOKEN, {
polling: false
});
console.log('π€ Telegram Bot initialized');
} catch (error) {
console.error('β Failed to initialize Telegram Bot:', error);
bot = null;
}
// Pexels API helper functions
async function pexelsRequest(endpoint: string, params: any = {}) {
const url = new URL(`https://api.pexels.com/v1${endpoint}`);
Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) {
url.searchParams.append(key, params[key].toString());
}
});
const response = await fetch(url.toString(), {
headers: {
'Authorization': PEXELS_API_KEY,
'User-Agent': 'Telegram-MCP-Server/2.1.0'
}
});
if (!response.ok) {
throw new Error(`Pexels API error: ${response.statusText}`);
}
return await response.json();
}
// Yandex Wordstat API helper functions
async function yandexWordstatRequest(method: string, params: any = {}) {
if (!YANDEX_OAUTH_TOKEN) {
throw new Error('Yandex OAuth token not set. Please authorize first using /yandex/auth endpoint');
}
const requestBody = {
method: method,
params: params
};
const response = await fetch('https://api.direct.yandex.com/json/v5/keywordsresearch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${YANDEX_OAUTH_TOKEN}`,
'Client-Login': YANDEX_LOGIN,
'Content-Type': 'application/json',
'Accept-Language': 'ru',
'skipReportHeader': 'true',
'skipColumnHeader': 'true'
},
body: JSON.stringify(requestBody)
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(`Yandex API error (${response.status}): ${responseText}`);
}
try {
return JSON.parse(responseText);
} catch (e) {
throw new Error(`Failed to parse Yandex API response: ${responseText}`);
}
}
// OAuth helper to exchange code for token
async function getYandexOAuthToken(code: string) {
const params = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: YANDEX_CLIENT_ID,
client_secret: YANDEX_CLIENT_SECRET
});
const response = await fetch('https://oauth.yandex.ru/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
if (!response.ok) {
throw new Error(`OAuth error: ${response.statusText}`);
}
const data = await response.json();
YANDEX_OAUTH_TOKEN = data.access_token;
return data;
}
// Root endpoint - GET
app.get('/', (req, res) => {
res.json({
name: 'telegram-mcp-server',
version: '2.2.0',
status: 'running',
description: 'Telegram MCP Server v2.2.0 with Pexels & Yandex Wordstat - HTTP API for ChatGPT and Make.com',
environment: {
TELEGRAM_BOT_TOKEN: TELEGRAM_BOT_TOKEN ? 'SET' : 'NOT SET',
TELEGRAM_CHANNEL_ID: CHANNEL_ID,
NODE_ENV: process.env.NODE_ENV || 'development',
PORT: process.env.PORT || 8080
},
endpoints: {
info: '/mcp',
tools: '/mcp/tools/list',
call: '/mcp/tools/call',
health: '/health'
},
channel: CHANNEL_ID
});
});
// MCP tools/list endpoint - GET (for direct access)
app.get('/tools/list', (req, res) => {
res.json({
jsonrpc: '2.0',
id: null,
result: {
tools: [
{
name: 'send_message',
description: 'Send a text message to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Message text to send',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the message (HTML or Markdown)',
},
},
required: ['text'],
},
},
{
name: 'send_photo',
description: 'Send a photo to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
photo: {
type: 'string',
description: 'Photo URL or file path',
},
caption: {
type: 'string',
description: 'Photo caption',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the caption',
},
},
required: ['photo'],
},
},
{
name: 'send_video',
description: 'Send a video to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
video: {
type: 'string',
description: 'Video URL or file path',
},
caption: {
type: 'string',
description: 'Video caption',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the caption',
},
},
required: ['video'],
},
},
{
name: 'send_document',
description: 'Send a document to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
document: {
type: 'string',
description: 'Document URL or file path',
},
caption: {
type: 'string',
description: 'Document caption',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the caption',
},
},
required: ['document'],
},
},
{
name: 'send_poll',
description: 'Send a poll to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'Poll question',
},
options: {
type: 'array',
items: { type: 'string' },
description: 'Poll options',
},
is_anonymous: {
type: 'boolean',
description: 'Whether the poll is anonymous',
},
},
required: ['question', 'options'],
},
},
{
name: 'send_reaction',
description: 'Send a reaction to a message',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to react to',
},
reaction: {
type: 'string',
description: 'Reaction emoji',
},
},
required: ['message_id', 'reaction'],
},
},
{
name: 'edit_message',
description: 'Edit a message text',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to edit',
},
text: {
type: 'string',
description: 'New message text',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the message',
},
},
required: ['message_id', 'text'],
},
},
{
name: 'delete_message',
description: 'Delete a message',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to delete',
},
},
required: ['message_id'],
},
},
{
name: 'pin_message',
description: 'Pin a message',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to pin',
},
},
required: ['message_id'],
},
},
{
name: 'unpin_message',
description: 'Unpin a message',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to unpin',
},
},
required: ['message_id'],
},
},
{
name: 'get_channel_info',
description: 'Get channel information',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_channel_stats',
description: 'Get channel statistics',
inputSchema: {
type: 'object',
properties: {},
},
},
]
}
});
});
// Root endpoint - POST (for Make.com MCP connection)
app.post('/', async (req, res) => {
console.log('π¨ POST request to root endpoint:', JSON.stringify(req.body, null, 2));
// Handle MCP protocol requests
const { jsonrpc, method, params, id } = req.body;
// Validate MCP request format
if (!jsonrpc || jsonrpc !== '2.0') {
console.log('β Invalid JSON-RPC version:', jsonrpc);
res.status(400).json({
jsonrpc: '2.0',
id: id || null,
error: {
code: -32600,
message: 'Invalid Request: jsonrpc must be "2.0"'
}
});
return;
}
if (!method) {
console.log('β Missing method in request');
res.status(400).json({
jsonrpc: '2.0',
id: id || null,
error: {
code: -32600,
message: 'Invalid Request: method is required'
}
});
return;
}
if (jsonrpc === '2.0' && method) {
// This is an MCP request
switch (method) {
case 'initialize':
res.json({
jsonrpc: '2.0',
id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {}
},
serverInfo: {
name: 'telegram-mcp-server',
version: '2.2.0'
}
}
});
break;
case 'tools/list':
res.json({
jsonrpc: '2.0',
id,
result: {
tools: [
{
name: 'send_message',
description: 'Send a text message to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Message text to send',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the message (HTML or Markdown)',
},
},
required: ['text'],
},
},
{
name: 'send_photo',
description: 'Send a photo to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
photo: {
type: 'string',
description: 'Photo URL or file path',
},
caption: {
type: 'string',
description: 'Photo caption',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the caption',
},
},
required: ['photo'],
},
},
{
name: 'send_video',
description: 'Send a video to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
video: {
type: 'string',
description: 'Video URL or file path',
},
caption: {
type: 'string',
description: 'Video caption',
},
duration: {
type: 'number',
description: 'Video duration in seconds',
},
width: {
type: 'number',
description: 'Video width',
},
height: {
type: 'number',
description: 'Video height',
},
},
required: ['video'],
},
},
{
name: 'send_document',
description: 'Send a document to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
document: {
type: 'string',
description: 'Document URL or file path',
},
caption: {
type: 'string',
description: 'Document caption',
},
filename: {
type: 'string',
description: 'Custom filename for the document',
},
},
required: ['document'],
},
},
{
name: 'send_poll',
description: 'Send a poll to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'Poll question',
},
options: {
type: 'array',
items: { type: 'string' },
description: 'Poll options (2-10 options)',
},
is_anonymous: {
type: 'boolean',
description: 'Whether the poll is anonymous',
},
type: {
type: 'string',
enum: ['quiz', 'regular'],
description: 'Poll type',
},
correct_option_id: {
type: 'number',
description: 'Correct option ID for quiz polls',
},
explanation: {
type: 'string',
description: 'Explanation for quiz polls',
},
},
required: ['question', 'options'],
},
},
{
name: 'send_reaction',
description: 'Send a reaction to a message',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to react to',
},
emoji: {
type: 'string',
description: 'Emoji to react with',
},
},
required: ['message_id', 'emoji'],
},
},
{
name: 'edit_message',
description: 'Edit a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to edit',
},
text: {
type: 'string',
description: 'New message text',
},
parse_mode: {
type: 'string',
enum: ['HTML', 'Markdown'],
description: 'Parse mode for the message',
},
},
required: ['message_id', 'text'],
},
},
{
name: 'delete_message',
description: 'Delete a message from the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to delete',
},
},
required: ['message_id'],
},
},
{
name: 'pin_message',
description: 'Pin a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to pin',
},
disable_notification: {
type: 'boolean',
description: 'Whether to disable notification for pinned message',
},
},
required: ['message_id'],
},
},
{
name: 'unpin_message',
description: 'Unpin a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: {
type: 'number',
description: 'Message ID to unpin (optional, unpins all if not specified)',
},
},
},
},
{
name: 'get_channel_info',
description: 'Get information about the Telegram channel',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_channel_stats',
description: 'Get channel statistics and member count',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'pexels_search_photos',
description: 'Search for photos on Pexels by query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (e.g., "nature", "cats", "city")',
},
per_page: {
type: 'number',
description: 'Number of results per page (1-80, default: 15)',
},
page: {
type: 'number',
description: 'Page number (default: 1)',
},
orientation: {
type: 'string',
enum: ['landscape', 'portrait', 'square'],
description: 'Photo orientation filter',
},
size: {
type: 'string',
enum: ['large', 'medium', 'small'],
description: 'Minimum photo size',
},
color: {
type: 'string',
description: 'Desired photo color (e.g., "red", "blue", "#FF0000")',
},
},
required: ['query'],
},
},
{
name: 'pexels_get_photo',
description: 'Get a specific photo by ID from Pexels',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'Photo ID from Pexels',
},
},
required: ['id'],
},
},
{
name: 'pexels_curated_photos',
description: 'Get curated photos from Pexels',
inputSchema: {
type: 'object',
properties: {
per_page: {
type: 'number',
description: 'Number of results per page (1-80, default: 15)',
},
page: {
type: 'number',
description: 'Page number (default: 1)',
},
},
},
},
{
name: 'pexels_search_videos',
description: 'Search for videos on Pexels by query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (e.g., "nature", "ocean", "city")',
},
per_page: {
type: 'number',
description: 'Number of results per page (1-80, default: 15)',
},
page: {
type: 'number',
description: 'Page number (default: 1)',
},
orientation: {
type: 'string',
enum: ['landscape', 'portrait', 'square'],
description: 'Video orientation filter',
},
size: {
type: 'string',
enum: ['large', 'medium', 'small'],
description: 'Minimum video size',
},
},
required: ['query'],
},
},
{
name: 'pexels_popular_videos',
description: 'Get popular videos from Pexels',
inputSchema: {
type: 'object',
properties: {
per_page: {
type: 'number',
description: 'Number of results per page (1-80, default: 15)',
},
page: {
type: 'number',
description: 'Page number (default: 1)',
},
},
},
},
{
name: 'yandex_wordstat_search',
description: 'Search Yandex Wordstat for keyword statistics',
inputSchema: {
type: 'object',
properties: {
phrases: {
type: 'array',
items: { type: 'string' },
description: 'Keywords to search (1-10 phrases)',
},
geo_ids: {
type: 'array',
items: { type: 'number' },
description: 'Geo IDs (225=Russia, 213=Moscow, 2=SPb)',
},
},
required: ['phrases'],
},
},
{
name: 'yandex_wordstat_keywords',
description: 'Get keyword suggestions from Yandex Wordstat',
inputSchema: {
type: 'object',
properties: {
phrase: {
type: 'string',
description: 'Base keyword for suggestions',
},
geo_ids: {
type: 'array',
items: { type: 'number' },
description: 'Geo IDs (default: [225] Russia)',
},
},
required: ['phrase'],
},
},
{
name: 'yandex_wordstat_related',
description: 'Get related search queries from Yandex Wordstat',
inputSchema: {
type: 'object',
properties: {
phrase: {
type: 'string',
description: 'Base keyword for related queries',
},
geo_ids: {
type: 'array',
items: { type: 'number' },
description: 'Geo IDs (default: [225] Russia)',
},
},
required: ['phrase'],
},
}
]
}
});
break;
case 'tools/call':
const { name, arguments: args } = params || {};
if (!name) {
res.json({
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: 'Tool name is required'
}
});
return;
}
let result;
switch (name) {
case 'send_message': {
const { text, parse_mode = 'HTML' } = args || {};
if (!text) {
result = {
success: false,
error: 'Text is required for send_message'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
// Ensure proper UTF-8 encoding
const encodedText = Buffer.from(text, 'utf8').toString('utf8');
const response = await bot.sendMessage(CHANNEL_ID, encodedText, {
parse_mode: parse_mode as any,
});
result = {
success: true,
message: 'Message sent to Telegram successfully!',
message_id: response.message_id,
text: response.text,
channel: CHANNEL_ID,
date: response.date,
timestamp: new Date(response.date * 1000).toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send message: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'send_photo': {
const { photo, caption, parse_mode = 'HTML' } = args || {};
if (!photo) {
result = {
success: false,
error: 'Photo URL is required for send_photo'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const response = await bot.sendPhoto(CHANNEL_ID, photo, {
caption,
parse_mode: parse_mode as any,
});
result = {
success: true,
message: 'Photo sent to Telegram successfully!',
message_id: response.message_id,
photo: response.photo,
caption: response.caption,
channel: CHANNEL_ID,
date: response.date,
timestamp: new Date(response.date * 1000).toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send photo: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'send_video': {
const { video, caption, duration, width, height, parse_mode = 'HTML' } = args || {};
if (!video) {
result = {
success: false,
error: 'Video URL is required for send_video'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const response = await bot.sendVideo(CHANNEL_ID, video, {
caption,
duration,
width,
height,
parse_mode: parse_mode as any,
});
result = {
success: true,
message: 'Video sent to Telegram successfully!',
message_id: response.message_id,
video: response.video,
caption: response.caption,
channel: CHANNEL_ID,
date: response.date,
timestamp: new Date(response.date * 1000).toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send video: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'send_document': {
const { document, caption, filename, parse_mode = 'HTML' } = args || {};
if (!document) {
result = {
success: false,
error: 'Document URL is required for send_document'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const response = await bot.sendDocument(CHANNEL_ID, document, {
caption,
parse_mode: parse_mode as any,
});
result = {
success: true,
message: 'Document sent to Telegram successfully!',
message_id: response.message_id,
document: response.document,
caption: response.caption,
channel: CHANNEL_ID,
date: response.date,
timestamp: new Date(response.date * 1000).toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send document: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'send_poll': {
const { question, options, is_anonymous = true, type = 'regular', correct_option_id, explanation } = args || {};
if (!question || !options || options.length < 2) {
result = {
success: false,
error: 'Question and at least 2 options are required for send_poll'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const pollOptions: any = {
question,
options,
is_anonymous,
type
};
if (type === 'quiz' && correct_option_id !== undefined) {
pollOptions.correct_option_id = correct_option_id;
if (explanation) {
pollOptions.explanation = explanation;
}
}
const response = await bot.sendPoll(CHANNEL_ID, question, options, pollOptions);
result = {
success: true,
message: 'Poll sent to Telegram successfully!',
message_id: response.message_id,
poll: response.poll,
channel: CHANNEL_ID,
date: response.date,
timestamp: new Date(response.date * 1000).toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send poll: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'send_reaction': {
const { message_id, emoji } = args || {};
if (!message_id || !emoji) {
result = {
success: false,
error: 'Message ID and emoji are required for send_reaction'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const response = await bot.setMessageReaction(CHANNEL_ID, message_id, { reaction: [{ type: 'emoji', emoji }] });
result = {
success: true,
message: 'Reaction sent successfully!',
message_id,
emoji,
channel: CHANNEL_ID
};
} catch (error: any) {
result = {
success: false,
error: `Failed to send reaction: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'edit_message': {
const { message_id, text, parse_mode = 'HTML' } = args || {};
if (!message_id || !text) {
result = {
success: false,
error: 'Message ID and text are required for edit_message'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
// Ensure proper UTF-8 encoding
const encodedText = Buffer.from(text, 'utf8').toString('utf8');
const response = await bot.editMessageText(encodedText, {
chat_id: CHANNEL_ID,
message_id,
parse_mode: parse_mode as any,
});
result = {
success: true,
message: 'Message edited successfully!',
message_id,
text: typeof response === 'object' && response ? response.text : text,
channel: CHANNEL_ID
};
} catch (error: any) {
result = {
success: false,
error: `Failed to edit message: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'delete_message': {
const { message_id } = args || {};
if (!message_id) {
result = {
success: false,
error: 'Message ID is required for delete_message'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
await bot.deleteMessage(CHANNEL_ID, message_id);
result = {
success: true,
message: 'Message deleted successfully!',
message_id,
channel: CHANNEL_ID
};
} catch (error: any) {
result = {
success: false,
error: `Failed to delete message: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'pin_message': {
const { message_id, disable_notification = false } = args || {};
if (!message_id) {
result = {
success: false,
error: 'Message ID is required for pin_message'
};
} else if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
await bot.pinChatMessage(CHANNEL_ID, message_id, { disable_notification });
result = {
success: true,
message: 'Message pinned successfully!',
message_id,
channel: CHANNEL_ID
};
} catch (error: any) {
result = {
success: false,
error: `Failed to pin message: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'unpin_message': {
const { message_id } = args || {};
if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
if (message_id) {
await bot.unpinChatMessage(CHANNEL_ID, message_id);
result = {
success: true,
message: 'Message unpinned successfully!',
message_id,
channel: CHANNEL_ID
};
} else {
await bot.unpinAllChatMessages(CHANNEL_ID);
result = {
success: true,
message: 'All messages unpinned successfully!',
channel: CHANNEL_ID
};
}
} catch (error: any) {
result = {
success: false,
error: `Failed to unpin message: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'get_channel_info': {
if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const chat = await bot.getChat(CHANNEL_ID);
result = {
success: true,
channel: CHANNEL_ID,
title: chat.title,
type: chat.type,
description: chat.description || 'No description',
username: chat.username || 'No username',
id: chat.id
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get channel info: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'get_channel_stats': {
if (!bot) {
result = {
success: false,
error: 'Telegram Bot not initialized. Check bot token.'
};
} else {
try {
const chat = await bot.getChat(CHANNEL_ID);
const memberCount = await bot.getChatMemberCount(CHANNEL_ID);
result = {
success: true,
channel: CHANNEL_ID,
title: chat.title,
member_count: memberCount,
type: chat.type,
username: chat.username || 'No username',
id: chat.id,
timestamp: new Date().toLocaleString()
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get channel stats: ${error.message}`,
details: error.response?.body || error.message
};
}
}
break;
}
case 'pexels_search_photos': {
const { query, per_page = 15, page = 1, orientation, size, color } = args || {};
if (!query) {
result = {
success: false,
error: 'Query is required for pexels_search_photos'
};
} else {
try {
const params: any = { query, per_page, page };
if (orientation) params.orientation = orientation;
if (size) params.size = size;
if (color) params.color = color;
const data = await pexelsRequest('/search', params);
result = {
success: true,
total_results: data.total_results,
page: data.page,
per_page: data.per_page,
photos: data.photos.map((photo: any) => ({
id: photo.id,
width: photo.width,
height: photo.height,
url: photo.url,
photographer: photo.photographer,
photographer_url: photo.photographer_url,
src: {
original: photo.src.original,
large: photo.src.large,
medium: photo.src.medium,
small: photo.src.small,
tiny: photo.src.tiny
},
alt: photo.alt
}))
};
} catch (error: any) {
result = {
success: false,
error: `Failed to search photos: ${error.message}`
};
}
}
break;
}
case 'pexels_get_photo': {
const { id } = args || {};
if (!id) {
result = {
success: false,
error: 'Photo ID is required for pexels_get_photo'
};
} else {
try {
const photo = await pexelsRequest(`/photos/${id}`);
result = {
success: true,
photo: {
id: photo.id,
width: photo.width,
height: photo.height,
url: photo.url,
photographer: photo.photographer,
photographer_url: photo.photographer_url,
src: {
original: photo.src.original,
large: photo.src.large,
medium: photo.src.medium,
small: photo.src.small,
tiny: photo.src.tiny
},
alt: photo.alt
}
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get photo: ${error.message}`
};
}
}
break;
}
case 'pexels_curated_photos': {
const { per_page = 15, page = 1 } = args || {};
try {
const data = await pexelsRequest('/curated', { per_page, page });
result = {
success: true,
page: data.page,
per_page: data.per_page,
photos: data.photos.map((photo: any) => ({
id: photo.id,
width: photo.width,
height: photo.height,
url: photo.url,
photographer: photo.photographer,
photographer_url: photo.photographer_url,
src: {
original: photo.src.original,
large: photo.src.large,
medium: photo.src.medium,
small: photo.src.small,
tiny: photo.src.tiny
},
alt: photo.alt
}))
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get curated photos: ${error.message}`
};
}
break;
}
case 'pexels_search_videos': {
const { query, per_page = 15, page = 1, orientation, size } = args || {};
if (!query) {
result = {
success: false,
error: 'Query is required for pexels_search_videos'
};
} else {
try {
const params: any = { query, per_page, page };
if (orientation) params.orientation = orientation;
if (size) params.size = size;
const data = await pexelsRequest('/videos/search', params);
result = {
success: true,
total_results: data.total_results,
page: data.page,
per_page: data.per_page,
videos: data.videos.map((video: any) => ({
id: video.id,
width: video.width,
height: video.height,
url: video.url,
duration: video.duration,
user: {
name: video.user.name,
url: video.user.url
},
video_files: video.video_files.map((file: any) => ({
id: file.id,
quality: file.quality,
file_type: file.file_type,
width: file.width,
height: file.height,
link: file.link
}))
}))
};
} catch (error: any) {
result = {
success: false,
error: `Failed to search videos: ${error.message}`
};
}
}
break;
}
case 'pexels_popular_videos': {
const { per_page = 15, page = 1 } = args || {};
try {
const data = await pexelsRequest('/videos/popular', { per_page, page });
result = {
success: true,
page: data.page,
per_page: data.per_page,
videos: data.videos.map((video: any) => ({
id: video.id,
width: video.width,
height: video.height,
url: video.url,
duration: video.duration,
user: {
name: video.user.name,
url: video.user.url
},
video_files: video.video_files.map((file: any) => ({
id: file.id,
quality: file.quality,
file_type: file.file_type,
width: file.width,
height: file.height,
link: file.link
}))
}))
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get popular videos: ${error.message}`
};
}
break;
}
case 'yandex_wordstat_search': {
const { phrases, geo_ids = [225] } = args || {};
if (!phrases || !Array.isArray(phrases) || phrases.length === 0) {
result = {
success: false,
error: 'Phrases array is required for yandex_wordstat_search (1-10 phrases)'
};
} else {
try {
const response = await yandexWordstatRequest('get', {
SelectionCriteria: {
GeoIds: geo_ids
},
Phrases: phrases,
FieldNames: ['Keyword', 'Shows']
});
result = {
success: true,
data: response.result || response,
phrases: phrases,
geo_ids: geo_ids,
full_api_response: response,
note: 'Wordstat data for specified phrases and regions'
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get Wordstat data: ${error.message}`
};
}
}
break;
}
case 'yandex_wordstat_keywords': {
const { phrase, geo_ids = [225] } = args || {};
if (!phrase) {
result = {
success: false,
error: 'Phrase is required for yandex_wordstat_keywords'
};
} else {
try {
const response = await yandexWordstatRequest('get', {
SelectionCriteria: {
GeoIds: geo_ids
},
Phrases: [phrase],
FieldNames: ['Keyword', 'Shows']
});
result = {
success: true,
phrase: phrase,
geo_ids: geo_ids,
full_response: response,
keywords: response.result || response,
note: 'Keyword suggestions based on Wordstat data'
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get keywords: ${error.message}`,
details: error.stack
};
}
}
break;
}
case 'yandex_wordstat_related': {
const { phrase, geo_ids = [225] } = args || {};
if (!phrase) {
result = {
success: false,
error: 'Phrase is required for yandex_wordstat_related'
};
} else {
try {
const response = await yandexWordstatRequest('get', {
SelectionCriteria: {
GeoIds: geo_ids
},
Phrases: [phrase],
FieldNames: ['Keyword', 'Shows']
});
// Extract related queries from response
const relatedQueries = response.result?.SearchedWith || [];
result = {
success: true,
phrase: phrase,
geo_ids: geo_ids,
full_response: response,
related_queries: relatedQueries,
count: relatedQueries.length,
note: 'Related search queries from Yandex Wordstat'
};
} catch (error: any) {
result = {
success: false,
error: `Failed to get related queries: ${error.message}`
};
}
}
break;
}
default:
result = {
success: false,
error: `Unknown tool: ${name}`
};
}
res.json({
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify(result)
}
]
}
});
break;
case 'notifications/initialized':
// MCP client initialization notification
res.json({
jsonrpc: '2.0',
id,
result: {}
});
break;
default:
res.json({
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Method not found: ${method}`
}
});
}
} else {
// Invalid MCP request format
console.log('β Invalid MCP request format:', req.body);
res.status(400).json({
jsonrpc: '2.0',
id: id || null,
error: {
code: -32600,
message: 'Invalid Request: not a valid MCP request'
}
});
}
});
// Health check endpoint
app.get('/health', async (req, res) => {
let bot_connected = false;
let bot_username = null;
if (bot) {
try {
const botInfo = await bot.getMe();
bot_connected = true;
bot_username = botInfo.username;
} catch (error) {
console.error('Bot health check failed:', error);
}
}
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
channel: CHANNEL_ID,
bot_token_set: !!TELEGRAM_BOT_TOKEN,
bot_connected: bot_connected,
bot_username: bot_username,
version: '2.2.0',
mcp_server: true,
pexels_enabled: !!PEXELS_API_KEY,
yandex_oauth: !!YANDEX_OAUTH_TOKEN,
environment: process.env.NODE_ENV || 'development'
});
});
// Yandex OAuth endpoints
app.get('/yandex/auth', (req, res) => {
const redirectUri = `${req.protocol}://${req.get('host')}/yandex/callback`;
const authUrl = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${YANDEX_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=direct:api`;
res.json({
message: 'Yandex OAuth Authorization with Direct API scope',
instructions: 'Open this URL in browser to authorize',
auth_url: authUrl,
callback_url: redirectUri,
scope: 'direct:api',
note: 'After authorization, you will be redirected to callback URL with code'
});
});
app.get('/yandex/callback', async (req, res) => {
const code = req.query.code as string;
if (!code) {
return res.status(400).json({
error: 'Authorization code is required',
message: 'No code provided in callback'
});
}
try {
const tokenData = await getYandexOAuthToken(code);
res.json({
success: true,
message: 'Yandex OAuth token obtained successfully!',
token_type: tokenData.token_type,
expires_in: tokenData.expires_in,
note: 'Token has been saved. You can now use Yandex Wordstat tools!'
});
} catch (error: any) {
res.status(500).json({
success: false,
error: error.message,
note: 'Failed to exchange code for token'
});
}
});
app.post('/yandex/set-token', (req, res) => {
const { token } = req.body;
if (!token) {
return res.status(400).json({
error: 'Token is required',
usage: 'POST /yandex/set-token with body: { "token": "your_token" }'
});
}
YANDEX_OAUTH_TOKEN = token;
res.json({
success: true,
message: 'Yandex OAuth token set successfully!',
note: 'You can now use Yandex Wordstat tools'
});
});
app.get('/yandex/status', (req, res) => {
const redirectUri = `${req.protocol}://${req.get('host')}/yandex/callback`;
res.json({
client_id: YANDEX_CLIENT_ID,
login: YANDEX_LOGIN,
token_set: !!YANDEX_OAUTH_TOKEN,
token_preview: YANDEX_OAUTH_TOKEN ? `${YANDEX_OAUTH_TOKEN.substring(0, 20)}...` : 'Not set',
auth_url: `https://oauth.yandex.ru/authorize?response_type=code&client_id=${YANDEX_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=direct:api`,
callback_url: redirectUri,
scope: 'direct:api'
});
});
app.get('/yandex/get-token', (req, res) => {
if (!YANDEX_OAUTH_TOKEN) {
return res.json({
error: 'Token not set',
message: 'Please authorize first'
});
}
res.json({
token: YANDEX_OAUTH_TOKEN,
note: 'Save this as YANDEX_OAUTH_TOKEN environment variable in Railway'
});
});
// MCP Server Info endpoint
app.get('/mcp', (req, res) => {
res.json({
name: 'telegram-mcp-server',
version: '2.1.0',
capabilities: {
tools: {}
}
});
});
// List tools endpoint
app.get('/mcp/tools/list', (req, res) => {
res.json({
tools: [
{
name: 'send_message',
description: 'Send a text message to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Message text to send' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown'], description: 'Parse mode for the message' }
},
required: ['text']
}
},
{
name: 'send_photo',
description: 'Send a photo to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
photo: { type: 'string', description: 'Photo URL or file path' },
caption: { type: 'string', description: 'Photo caption' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown'], description: 'Parse mode for the caption' }
},
required: ['photo']
}
},
{
name: 'send_video',
description: 'Send a video to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
video: { type: 'string', description: 'Video URL or file path' },
caption: { type: 'string', description: 'Video caption' },
duration: { type: 'number', description: 'Video duration in seconds' },
width: { type: 'number', description: 'Video width' },
height: { type: 'number', description: 'Video height' }
},
required: ['video']
}
},
{
name: 'send_document',
description: 'Send a document to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
document: { type: 'string', description: 'Document URL or file path' },
caption: { type: 'string', description: 'Document caption' },
filename: { type: 'string', description: 'Custom filename for the document' }
},
required: ['document']
}
},
{
name: 'send_poll',
description: 'Send a poll to the Telegram channel',
inputSchema: {
type: 'object',
properties: {
question: { type: 'string', description: 'Poll question' },
options: { type: 'array', items: { type: 'string' }, description: 'Poll options (2-10 options)' },
is_anonymous: { type: 'boolean', description: 'Whether the poll is anonymous' },
type: { type: 'string', enum: ['quiz', 'regular'], description: 'Poll type' },
correct_option_id: { type: 'number', description: 'Correct option ID for quiz polls' },
explanation: { type: 'string', description: 'Explanation for quiz polls' }
},
required: ['question', 'options']
}
},
{
name: 'send_reaction',
description: 'Send a reaction to a message',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'number', description: 'Message ID to react to' },
emoji: { type: 'string', description: 'Emoji to react with' }
},
required: ['message_id', 'emoji']
}
},
{
name: 'edit_message',
description: 'Edit a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'number', description: 'Message ID to edit' },
text: { type: 'string', description: 'New message text' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown'], description: 'Parse mode for the message' }
},
required: ['message_id', 'text']
}
},
{
name: 'delete_message',
description: 'Delete a message from the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'number', description: 'Message ID to delete' }
},
required: ['message_id']
}
},
{
name: 'pin_message',
description: 'Pin a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'number', description: 'Message ID to pin' },
disable_notification: { type: 'boolean', description: 'Whether to disable notification for pinned message' }
},
required: ['message_id']
}
},
{
name: 'unpin_message',
description: 'Unpin a message in the Telegram channel',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'number', description: 'Message ID to unpin (optional, unpins all if not specified)' }
}
}
},
{
name: 'get_channel_info',
description: 'Get information about the Telegram channel',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_channel_stats',
description: 'Get channel statistics and member count',
inputSchema: {
type: 'object',
properties: {}
}
}
]
});
});
// Call tool endpoint
app.post('/mcp/tools/call', async (req, res) => {
try {
const { name, arguments: args } = req.body;
if (!name) {
return res.status(400).json({
error: 'Tool name is required',
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Tool name is required',
}),
},
],
isError: true,
});
}
let result;
switch (name) {
case 'send_message': {
const { text } = args || {};
if (!text) {
throw new Error('Text is required for send_message');
}
result = {
success: true,
message: 'Message would be sent to Telegram',
text: text,
channel: CHANNEL_ID,
note: 'This is a test response. Telegram integration requires proper bot setup.'
};
break;
}
case 'get_channel_info': {
result = {
success: true,
channel: CHANNEL_ID,
title: 'Test Channel',
type: 'channel',
description: 'This is a test response. Real channel info requires proper bot setup.',
note: 'This is a test response. Real channel info requires proper bot setup.'
};
break;
}
default:
throw new Error(`Unknown tool: ${name}`);
}
res.json({
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
});
} catch (error: any) {
console.error('Tool execution error:', error);
res.status(500).json({
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
tool: req.body.name,
}),
},
],
isError: true,
});
}
});
// Start the server
async function main() {
try {
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log('π Telegram MCP Server v2.2.0 with Pexels & Yandex Wordstat running on port', port);
console.log(`π API URL: http://localhost:${port}`);
console.log(`π MCP Info: http://localhost:${port}/mcp`);
console.log(`π§ Tools: http://localhost:${port}/mcp/tools/list`);
console.log(`π Health: http://localhost:${port}/health`);
console.log('β
Ready for testing!');
console.log(`π± Target channel: ${CHANNEL_ID}`);
console.log(`π Bot token: ${TELEGRAM_BOT_TOKEN ? 'SET' : 'NOT SET'}`);
});
} catch (error) {
console.error('β Server startup error:', error);
process.exit(1);
}
}
main().catch((error) => {
console.error('β Fatal error:', error);
process.exit(1);
});