@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
text/typescript
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();