UNPKG

@mcp-registry/mcp-telegram-ai-assistant

Version:

Professional MCP server for AI-powered Telegram communication - Perfect for students, educators, and researchers

346 lines (302 loc) 11.8 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import dotenv from 'dotenv'; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from 'zod'; import { GoogleGenerativeAI } from "@google/generative-ai"; import fs from "node:fs"; import path from "path"; import TelegramBot from 'node-telegram-bot-api'; // Load environment variables dotenv.config(); // Gemini API setup const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { throw new Error('GEMINI_API_KEY environment variable is not set'); } // Initialize the Google Generative AI client const genAI = new GoogleGenerativeAI(apiKey); // Use Gemini Flash 2 model const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash", generationConfig: { temperature: 0.7, topP: 0.95, topK: 64, maxOutputTokens: 65536 } }); // Ensure output directory exists const outputDir = path.join(process.cwd(), 'output'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Create server instance const server = new Server( { name: "mcp-telegram-ai-assistant", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Schema for generate-thinking tool const GenerateThinkingSchema = z.object({ prompt: z.string().describe('Prompt for generating thinking process text'), outputDir: z.string().optional().describe('Directory to save output responses'), sendToTelegram: z.boolean().optional().default(true).describe('Whether to send the response to Telegram') }); // Schema for send-telegram tool const SendTelegramSchema = z.object({ message: z.string().describe('Message to send to Telegram'), parseMode: z.enum(['Markdown', 'HTML']).optional().default('Markdown').describe('Parse mode for the message') }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "generate-thinking", description: "Generate detailed thinking process text using Gemini Flash 2 model and send to Telegram", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "Prompt for generating thinking process text", }, outputDir: { type: "string", description: "Directory to save output responses (optional)", }, sendToTelegram: { type: "boolean", description: "Whether to send the response to Telegram (optional, defaults to true)", }, }, required: ["prompt"], }, }, { name: "send-telegram", description: "Send a message directly to Telegram", inputSchema: { type: "object", properties: { message: { type: "string", description: "Message to send to Telegram", }, parseMode: { type: "string", enum: ["Markdown", "HTML"], description: "Parse mode for the message (optional, defaults to Markdown)", }, }, required: ["message"], }, } ], }; }); // Helper function to escape Markdown characters function escapeMarkdown(text: string): string { return text.replace(/([_*\[\]()~`>#+\-=|{}.!])/g, '\\$1'); } // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "generate-thinking": { const { prompt, outputDir: customOutputDir, sendToTelegram } = GenerateThinkingSchema.parse(args); const saveDir = customOutputDir ? path.resolve(customOutputDir) : outputDir; // Create directory if it doesn't exist if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // Generate content with Gemini console.error(`Sending prompt to Gemini: "${prompt}"`); const result = await model.generateContent(prompt); const responseText = result.response.text(); console.error(`Received response from Gemini (${responseText.length} chars)`); // Save the response to a file const timestamp = Date.now(); const filename = `gemini_thinking_${timestamp}.txt`; const filePath = path.join(saveDir, filename); fs.writeFileSync(filePath, responseText); console.error(`Saved response to: ${filePath}`); // Send to Telegram if configured if (sendToTelegram && telegramBot && telegramChatId) { try { // Format message for Telegram, escaping special characters const escapedResponse = escapeMarkdown(responseText); const escapedPrompt = escapeMarkdown(prompt); const telegramMessage = `*Generated Response*\n\n${escapedResponse}\n\n_Generated based on prompt:_ "${escapedPrompt}"`; // Send as plain text if contains Arabic or other RTL text const containsRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(responseText); await telegramBot.sendMessage(telegramChatId, telegramMessage, { parse_mode: containsRTL ? undefined : 'Markdown' }); console.error('Response sent to Telegram'); } catch (error) { console.error('Error sending to Telegram:', error); // Fallback to plain text if Markdown fails await telegramBot.sendMessage(telegramChatId, responseText); } } // Format the response as HTML for the tool response const styledResponse = ` <div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.5; color: #333;"> <div style="background-color: #f0f8ff; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 5px solid #4169e1;"> <h2 style="margin-top: 0; color: #4169e1;">Generated Response</h2> <p style="font-style: italic; color: #666;">Based on prompt: "${prompt}"</p> </div> <div style="background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"> <pre style="white-space: pre-wrap; word-wrap: break-word;">${responseText}</pre> </div> <div style="background-color: #f5f5f5; padding: 10px; border-radius: 8px; margin-top: 20px; font-size: 0.9em; color: #666;"> <p>Response saved to: ${filePath}</p> </div> </div>`; return { content: [ { type: "text", text: styledResponse, }, ], }; } case "send-telegram": { const { message, parseMode } = SendTelegramSchema.parse(args); if (!telegramBot || !telegramChatId) { throw new Error('Telegram bot is not configured. Please check your TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID.'); } try { // Send message to Telegram const sentMessage = await telegramBot.sendMessage(telegramChatId, message, { parse_mode: parseMode }); return { content: [ { type: "text", text: `<div style="font-family: Arial, sans-serif; padding: 20px; border-radius: 10px; border: 1px solid #e0e0e0; background-color: #f9f9f9;"> <div style="background-color: #4CAF50; color: white; padding: 10px 15px; border-radius: 5px; margin-bottom: 15px;"> <h2 style="margin: 0; font-size: 18px;">✅ Message Sent to Telegram</h2> </div> <div style="padding: 10px; background-color: white; border-radius: 5px; margin-bottom: 15px;"> <p><strong>Message ID:</strong> ${sentMessage.message_id}</p> <p><strong>Parse Mode:</strong> ${parseMode}</p> </div> </div>` } ] }; } catch (error) { console.error('Error sending Telegram message:', error); throw error; } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`Error in tool execution:`, error); throw error; } }); // Telegram Bot setup with polling const telegramToken = process.env.TELEGRAM_BOT_TOKEN; const telegramChatId = process.env.TELEGRAM_CHAT_ID; let telegramBot: TelegramBot | null = null; if (telegramToken && telegramChatId) { telegramBot = new TelegramBot(telegramToken, { polling: true }); // Handle /start command telegramBot.onText(/\/start/, async (msg) => { const chatId = msg.chat.id; await telegramBot!.sendMessage(chatId, "👋 Welcome to MCP Gemini Content Bot!\n\n" + "Available commands:\n" + "/think <prompt> - Generate content using Gemini\n" + "/help - Show this help message" ); }); // Handle /help command telegramBot.onText(/\/help/, async (msg) => { const chatId = msg.chat.id; await telegramBot!.sendMessage(chatId, "🤖 *MCP Gemini Content Bot Help*\n\n" + "*Commands:*\n" + "• /think <prompt> - Generate content using Gemini\n" + "• /start - Start the bot\n" + "• /help - Show this help message\n\n" + "*Examples:*\n" + "/think Explain quantum computing\n" + "/think Write a poem about technology", { parse_mode: "Markdown" } ); }); // Handle /think command telegramBot.onText(/\/think (.+)/, async (msg, match) => { const chatId = msg.chat.id; const prompt = match ? match[1] : ''; if (!prompt) { await telegramBot!.sendMessage(chatId, "Please provide a prompt after /think"); return; } try { // Send "thinking" message const thinkingMsg = await telegramBot!.sendMessage(chatId, "🤔 Thinking..."); // Generate content const result = await model.generateContent(prompt); const responseText = result.response.text(); // Format the response, checking for RTL content const containsRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(responseText); const formattedResponse = containsRTL ? `Generated Response\n\n${responseText}\n\nPrompt: ${prompt}` : `*Generated Response*\n\n${escapeMarkdown(responseText)}\n\n_Prompt: ${escapeMarkdown(prompt)}_`; // Edit the "thinking" message with the response await telegramBot!.editMessageText(formattedResponse, { chat_id: chatId, message_id: thinkingMsg.message_id, parse_mode: containsRTL ? undefined : 'Markdown' }); // Save response to file const timestamp = Date.now(); const filename = `gemini_thinking_${timestamp}.txt`; const filePath = path.join(outputDir, filename); fs.writeFileSync(filePath, `Prompt: ${prompt}\n\nResponse:\n${responseText}`); } catch (error) { console.error('Error generating content:', error); await telegramBot!.sendMessage(chatId, "❌ Sorry, I encountered an error while generating content. Please try again later." ); } }); // Handle errors telegramBot.on('error', (error) => { console.error('Telegram Bot Error:', error); }); console.error('Telegram bot is running...'); } // Start the server async function main() { try { // Start MCP server const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Gemini Content Server running on stdio"); } catch (error) { console.error("Fatal error in main():", error); process.exit(1); } } main();