feat: Claude Telegram Bridge MCP server

A Model Context Protocol (MCP) server that enables Claude to communicate
with users via Telegram. Provides two-way communication, notifications,
question/answer flows, and message queuing.

Features:
- MCP server implementation with 5 tools
- HTTP bridge for Telegram Bot API
- Real-time notifications with priority levels
- Question/answer blocking flow
- Message queue for async communication
- Background daemon support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
RichardDillman
2025-11-23 00:55:30 -05:00
commit 6c8c9350a1
13 changed files with 3146 additions and 0 deletions

284
src/index.ts Normal file
View File

@@ -0,0 +1,284 @@
import { Telegraf } from 'telegraf';
import express from 'express';
import dotenv from 'dotenv';
import fs from 'fs/promises';
import path from 'path';
dotenv.config();
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
const app = express();
const PORT = parseInt(process.env.PORT || '3456');
const HOST = process.env.HOST || 'localhost';
const ENABLED = process.env.ENABLED !== 'false';
let chatId: string | null = process.env.TELEGRAM_CHAT_ID || null;
const envPath = path.join(process.cwd(), '.env');
// Message queue for two-way communication
interface QueuedMessage {
from: string;
message: string;
timestamp: Date;
read: boolean;
}
const messageQueue: QueuedMessage[] = [];
const pendingQuestions = new Map<string, { resolve: (answer: string) => void; timeout: NodeJS.Timeout }>();
app.use(express.json());
// Save chat ID to .env file
async function saveChatId(id: string) {
try {
const envContent = await fs.readFile(envPath, 'utf-8');
const updated = envContent.replace(
/TELEGRAM_CHAT_ID=.*/,
`TELEGRAM_CHAT_ID=${id}`
);
await fs.writeFile(envPath, updated);
console.log(`✅ Chat ID saved: ${id}`);
} catch (error) {
console.error('Failed to save chat ID:', error);
}
}
// Bot commands
bot.start(async (ctx) => {
chatId = ctx.chat.id.toString();
await saveChatId(chatId);
await ctx.reply(
'🤖 *Claude Telegram Bridge Active*\n\n' +
'I will now forward notifications from Claude Code and other apps.\n\n' +
'*Commands:*\n' +
'/status - Check bridge status\n' +
'/enable - Enable notifications\n' +
'/disable - Disable notifications\n' +
'/test - Send test notification',
{ parse_mode: 'Markdown' }
);
});
bot.command('status', async (ctx) => {
const status = ENABLED ? '✅ Enabled' : '⛔ Disabled';
await ctx.reply(
`*Bridge Status*\n\n` +
`Status: ${status}\n` +
`Chat ID: ${chatId}\n` +
`HTTP Server: http://${HOST}:${PORT}`,
{ parse_mode: 'Markdown' }
);
});
bot.command('help', async (ctx) => {
await ctx.reply(
'*Claude Telegram Bridge - Commands*\n\n' +
'*Bot Commands:*\n' +
'`/start` - Initialize and connect\n' +
'`/help` - Show this help message\n' +
'`/status` - Check bridge status\n' +
'`/test` - Send test notification\n\n' +
'*How it works:*\n' +
'• Send me any message - I forward it to Claude\n' +
'• Claude processes it and replies back\n' +
'• When Claude asks a question, your next message answers it\n\n' +
'*Features:*\n' +
'✅ Two-way communication\n' +
'✅ Question/Answer flow\n' +
'✅ Progress notifications\n' +
'✅ Error alerts\n\n' +
'More info: See README in bridge folder',
{ parse_mode: 'Markdown' }
);
});
bot.command('test', async (ctx) => {
await ctx.reply('✅ Test notification received! Bridge is working.');
});
// Listen for any text messages from user
bot.on('text', async (ctx) => {
const message = ctx.message.text;
const from = ctx.from.username || ctx.from.first_name;
console.log(`\n📨 Message from ${from}: "${message}"\n`);
// Check if this is an answer to a pending question
const questionId = Array.from(pendingQuestions.keys())[0];
if (questionId && pendingQuestions.has(questionId)) {
const { resolve, timeout } = pendingQuestions.get(questionId)!;
clearTimeout(timeout);
pendingQuestions.delete(questionId);
resolve(message);
await ctx.reply('✅ Answer received!');
return;
}
// Add to message queue for processing
messageQueue.push({
from,
message,
timestamp: new Date(),
read: false
});
// Acknowledge receipt - Claude will respond when available
await ctx.reply('💬 Message received - responding...');
console.log('📥 Queued for Claude to process');
});
// HTTP endpoint for sending notifications
app.post('/notify', async (req, res) => {
if (!ENABLED) {
return res.status(503).json({ error: 'Bridge is disabled' });
}
if (!chatId) {
return res.status(400).json({
error: 'No chat ID set. Please message the bot first with /start'
});
}
const { message, priority = 'info', parseMode = 'Markdown' } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
try {
const emojiMap: Record<string, string> = {
info: '',
success: '✅',
warning: '⚠️',
error: '❌',
question: '❓'
};
const emoji = emojiMap[priority] || '';
await bot.telegram.sendMessage(
chatId,
`${emoji} ${message}`,
{ parse_mode: parseMode as any }
);
res.json({ success: true, chatId });
} catch (error: any) {
console.error('Failed to send message:', error);
res.status(500).json({ error: error.message });
}
});
// Get unread messages
app.get('/messages', (req, res) => {
const unread = messageQueue.filter(m => !m.read);
res.json({ messages: unread, count: unread.length });
});
// Mark messages as read
app.post('/messages/read', (req, res) => {
const { count } = req.body;
const toMark = count || messageQueue.filter(m => !m.read).length;
let marked = 0;
for (const msg of messageQueue) {
if (!msg.read && marked < toMark) {
msg.read = true;
marked++;
}
}
res.json({ markedAsRead: marked });
});
// Send reply to user message
app.post('/reply', async (req, res) => {
if (!chatId) {
return res.status(400).json({ error: 'No chat ID set' });
}
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
try {
await bot.telegram.sendMessage(chatId, message, { parse_mode: 'Markdown' });
res.json({ success: true });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// Ask a question and wait for answer
app.post('/ask', async (req, res) => {
if (!chatId) {
return res.status(400).json({ error: 'No chat ID set' });
}
const { question, timeout = 300000 } = req.body; // 5 min default timeout
if (!question) {
return res.status(400).json({ error: 'Question is required' });
}
try {
const questionId = Date.now().toString();
// Send question to Telegram
await bot.telegram.sendMessage(chatId, `${question}`, { parse_mode: 'Markdown' });
// Wait for answer
const answer = await new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => {
pendingQuestions.delete(questionId);
reject(new Error('Timeout waiting for answer'));
}, timeout);
pendingQuestions.set(questionId, { resolve, timeout: timer });
});
res.json({ answer });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'running',
enabled: ENABLED,
chatId: chatId ? 'set' : 'not set',
unreadMessages: messageQueue.filter(m => !m.read).length,
pendingQuestions: pendingQuestions.size
});
});
// Start bot
bot.launch().then(() => {
console.log('🤖 Telegram bot started');
console.log('📱 Message your bot to get started');
});
// Start HTTP server
app.listen(PORT, HOST, () => {
console.log(`🌐 HTTP server running on http://${HOST}:${PORT}`);
console.log(`\n📋 Send notifications with:\n`);
console.log(`curl -X POST http://${HOST}:${PORT}/notify \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(` -d '{"message": "Hello from Claude!", "priority": "info"}'`);
});
// Graceful shutdown
process.once('SIGINT', () => {
console.log('\n👋 Shutting down...');
bot.stop('SIGINT');
process.exit(0);
});
process.once('SIGTERM', () => {
bot.stop('SIGTERM');
process.exit(0);
});

265
src/mcp-server.ts Normal file
View File

@@ -0,0 +1,265 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
const BRIDGE_URL = process.env.TELEGRAM_BRIDGE_URL || 'http://localhost:3456';
// Define the Telegram bridge tools
const TOOLS: Tool[] = [
{
name: 'telegram_notify',
description: 'Send a notification to the user via Telegram. Use this to keep the user informed about progress, completion, warnings, or errors.',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'The notification message to send. Supports Markdown formatting.',
},
priority: {
type: 'string',
enum: ['info', 'success', 'warning', 'error', 'question'],
description: 'Priority level: info ( general), success (✅ completed), warning (⚠️ alert), error (❌ failure), question (❓ needs input)',
default: 'info',
},
},
required: ['message'],
},
},
{
name: 'telegram_ask',
description: 'Ask the user a question via Telegram and wait for their answer. This blocks until the user responds. Use for decisions that require user input.',
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'The question to ask the user. Supports Markdown formatting.',
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds (default: 300000 = 5 minutes)',
default: 300000,
},
},
required: ['question'],
},
},
{
name: 'telegram_get_messages',
description: 'Retrieve unread messages from the user. Use this to check if the user has sent any messages.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'telegram_reply',
description: 'Send a reply to a user message via Telegram. Use after getting messages to respond to the user.',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'The reply message. Supports Markdown formatting.',
},
},
required: ['message'],
},
},
{
name: 'telegram_check_health',
description: 'Check the health and status of the Telegram bridge. Returns connection status, unread message count, and pending questions.',
inputSchema: {
type: 'object',
properties: {},
},
},
];
// Create the MCP server
const server = new Server(
{
name: 'telegram-bridge',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'telegram_notify': {
const { message, priority = 'info' } = args as { message: string; priority?: string };
const response = await fetch(`${BRIDGE_URL}/notify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, priority }),
});
if (!response.ok) {
const error: any = await response.json();
throw new Error(error.error || 'Failed to send notification');
}
return {
content: [
{
type: 'text',
text: `✅ Notification sent successfully to Telegram (priority: ${priority})`,
},
],
};
}
case 'telegram_ask': {
const { question, timeout = 300000 } = args as { question: string; timeout?: number };
const response = await fetch(`${BRIDGE_URL}/ask`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question, timeout }),
});
if (!response.ok) {
const error: any = await response.json();
throw new Error(error.error || 'Failed to ask question');
}
const result: any = await response.json();
return {
content: [
{
type: 'text',
text: `User's answer: ${result.answer}`,
},
],
};
}
case 'telegram_get_messages': {
const response = await fetch(`${BRIDGE_URL}/messages`);
if (!response.ok) {
const error: any = await response.json();
throw new Error(error.error || 'Failed to get messages');
}
const result: any = await response.json();
const messages = result.messages.map((m: any) =>
`[${m.timestamp}] ${m.from}: ${m.message}`
).join('\n');
return {
content: [
{
type: 'text',
text: result.count > 0
? `📬 ${result.count} unread message(s):\n\n${messages}`
: '📭 No unread messages',
},
],
};
}
case 'telegram_reply': {
const { message } = args as { message: string };
const response = await fetch(`${BRIDGE_URL}/reply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
if (!response.ok) {
const error: any = await response.json();
throw new Error(error.error || 'Failed to send reply');
}
return {
content: [
{
type: 'text',
text: '✅ Reply sent successfully to Telegram',
},
],
};
}
case 'telegram_check_health': {
const response = await fetch(`${BRIDGE_URL}/health`);
if (!response.ok) {
throw new Error('Bridge is not responding');
}
const health: any = await response.json();
const statusText = [
`🏥 Telegram Bridge Health Check`,
``,
`Status: ${health.status}`,
`Enabled: ${health.enabled ? '✅' : '❌'}`,
`Chat ID: ${health.chatId}`,
`Unread Messages: ${health.unreadMessages}`,
`Pending Questions: ${health.pendingQuestions}`,
].join('\n');
return {
content: [
{
type: 'text',
text: statusText,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: `❌ Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('🚀 Telegram MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});