From 163fb0708d78475dfad811c3f3cbd50e38f7e356 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 31 Mar 2026 17:13:17 +0700 Subject: [PATCH] implement MCP connector with Streamable HTTP transport Replaces simple Telegram forwarder with a proper MCP server deployable to Cloudflare Workers and connectable from Claude.ai web interface. --- index.js | 283 +++++++++++++++++++++++++++----------------------- wrangler.toml | 5 + 2 files changed, 157 insertions(+), 131 deletions(-) diff --git a/index.js b/index.js index 54177fa..950dc5f 100644 --- a/index.js +++ b/index.js @@ -1,151 +1,172 @@ +const CORS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Mcp-Session-Id', +}; + +const SERVER_INFO = { name: 'telegram-mcp', version: '1.0.0' }; +const PROTOCOL_VERSION = '2024-11-05'; + +const TOOLS = [ + { + name: 'send_message', + description: 'Send a message to the configured Telegram chat', + inputSchema: { + type: 'object', + properties: { + text: { type: 'string', description: 'Message text (supports HTML formatting)' }, + parse_mode: { type: 'string', enum: ['HTML', 'Markdown', 'MarkdownV2'], description: 'Text formatting mode (default: HTML)' }, + }, + required: ['text'], + }, + }, + { + name: 'get_updates', + description: 'Get recent pending updates (messages) received by the bot', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max number of updates to retrieve (1-100, default 10)' }, + }, + }, + }, + { + name: 'get_bot_info', + description: 'Get information about the configured Telegram bot', + inputSchema: { type: 'object', properties: {} }, + }, +]; + +function json(id, result) { + return new Response(JSON.stringify({ jsonrpc: '2.0', id, result }), { + headers: { 'Content-Type': 'application/json', ...CORS }, + }); +} + +function error(id, code, message) { + return new Response(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }), { + headers: { 'Content-Type': 'application/json', ...CORS }, + }); +} + +async function tg(token, method, body) { + const res = await fetch(`https://api.telegram.org/bot${token}/${method}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return res.json(); +} + export default { - async fetch(request, env, ctx) { - // Handle CORS preflight requests + async fetch(request, env) { if (request.method === 'OPTIONS') { - return new Response(null, { - status: 204, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - 'Access-Control-Max-Age': '86400', - }, - }); + return new Response(null, { status: 204, headers: CORS }); } if (request.method !== 'POST') { - return new Response('Method not allowed', { - status: 405, - headers: { - 'Access-Control-Allow-Origin': '*', - } - }); + return new Response('Method Not Allowed', { status: 405, headers: CORS }); } + if (env.MCP_SECRET) { + const auth = request.headers.get('Authorization'); + if (auth !== `Bearer ${env.MCP_SECRET}`) { + return new Response('Unauthorized', { status: 401, headers: CORS }); + } + } + + let body; try { - const contentType = request.headers.get('content-type'); - let text; + body = await request.json(); + } catch { + return error(null, -32700, 'Parse error'); + } - if (contentType && contentType.includes('application/json')) { - const body = await request.json(); - text = body.text; - } else if (contentType && contentType.includes('application/x-www-form-urlencoded')) { - const formData = await request.formData(); - text = formData.get('text'); - } else { - return new Response('Content-Type must be application/json or application/x-www-form-urlencoded', { - status: 400, - headers: { - 'Access-Control-Allow-Origin': '*', - } + const { id, method, params } = body; + + // Notifications (no id) — no response needed + if (id === undefined) { + return new Response(null, { status: 202, headers: CORS }); + } + + switch (method) { + case 'initialize': + return json(id, { + protocolVersion: PROTOCOL_VERSION, + capabilities: { tools: {} }, + serverInfo: SERVER_INFO, }); - } - if (!text) { - return new Response('Missing text parameter', { - status: 400, - headers: { - 'Access-Control-Allow-Origin': '*', - } - }); - } + case 'ping': + return json(id, {}); - // Collect client request information - const clientIP = request.headers.get('CF-Connecting-IP') || - request.headers.get('X-Forwarded-For') || - request.headers.get('X-Real-IP') || - 'Unknown'; + case 'tools/list': + return json(id, { tools: TOOLS }); - const userAgent = request.headers.get('User-Agent') || 'Unknown'; - const country = request.cf?.country || 'Unknown'; - const city = request.cf?.city || 'Unknown'; - const region = request.cf?.region || 'Unknown'; - const latitude = request.cf?.latitude || 'Unknown'; - const longitude = request.cf?.longitude || 'Unknown'; - const timezone = request.cf?.timezone || 'Unknown'; - const url = request.url || 'Unknown'; - const timestamp = new Date().toISOString(); + case 'tools/call': { + const { name, arguments: args = {} } = params ?? {}; + const { TELEGRAM_TOKEN: token, TELEGRAM_CHAT_ID: chatId } = env; - // Format the message with request information - const hasCoordinates = latitude !== 'Unknown' && longitude !== 'Unknown'; - const mapLink = hasCoordinates - ? `https://www.google.com/maps?q=${latitude},${longitude}` - : null; - - const requestInfo = `IP: ${clientIP} 🔍 -Browser: ${userAgent} -Country: ${country} -Region: ${region} -City: ${city} -Coordinates: ${latitude}, ${longitude}${mapLink ? ` 📍` : ''} -Timezone: ${timezone} -Timestamp: ${timestamp} -Original text:`; - - const formattedMessage = `${requestInfo} - -${text}`; - - const telegramToken = env.TELEGRAM_TOKEN; - const telegramChatId = env.TELEGRAM_CHAT_ID; - - if (!telegramToken || !telegramChatId) { - return new Response('Missing TELEGRAM_TOKEN or TELEGRAM_CHAT_ID environment variables', { - status: 500, - headers: { - 'Access-Control-Allow-Origin': '*', - } - }); - } - - const telegramUrl = `https://api.telegram.org/bot${telegramToken}/sendMessage`; - const telegramPayload = { - chat_id: telegramChatId, - text: formattedMessage, - parse_mode: 'HTML' - }; - - const telegramResponse = await fetch(telegramUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(telegramPayload) - }); - - const telegramResult = await telegramResponse.json(); - - if (!telegramResponse.ok) { - return new Response(JSON.stringify({ - success: false - }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - } - }); - } - - return new Response(JSON.stringify({ - success: true - }), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', + if (!token || !chatId) { + return json(id, { + content: [{ type: 'text', text: 'Error: TELEGRAM_TOKEN or TELEGRAM_CHAT_ID not set' }], + isError: true, + }); } - }); - } catch (error) { - return new Response(JSON.stringify({ - success: false - }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', + if (name === 'send_message') { + const result = await tg(token, 'sendMessage', { + chat_id: chatId, + text: args.text, + parse_mode: args.parse_mode ?? 'HTML', + }); + return json(id, { + content: [{ type: 'text', text: result.ok ? 'Message sent.' : `Error: ${result.description}` }], + isError: !result.ok, + }); } - }); + + if (name === 'get_updates') { + const result = await tg(token, 'getUpdates', { limit: Math.min(args.limit ?? 10, 100) }); + if (!result.ok) { + return json(id, { + content: [{ type: 'text', text: `Error: ${result.description}` }], + isError: true, + }); + } + const messages = result.result + .filter(u => u.message?.text) + .map(u => { + const { message: m } = u; + const from = m.from ? `${m.from.first_name}${m.from.username ? ` (@${m.from.username})` : ''}` : 'Unknown'; + const time = new Date(m.date * 1000).toISOString(); + return `[${time}] ${from}: ${m.text}`; + }) + .join('\n'); + return json(id, { + content: [{ type: 'text', text: messages || 'No pending messages.' }], + }); + } + + if (name === 'get_bot_info') { + const result = await tg(token, 'getMe', {}); + if (!result.ok) { + return json(id, { + content: [{ type: 'text', text: `Error: ${result.description}` }], + isError: true, + }); + } + const { first_name, username, id: botId } = result.result; + return json(id, { + content: [{ type: 'text', text: `Bot: ${first_name} (@${username}), ID: ${botId}` }], + }); + } + + return error(id, -32601, `Unknown tool: ${name}`); + } + + default: + return error(id, -32601, `Method not found: ${method}`); } }, }; diff --git a/wrangler.toml b/wrangler.toml index 0735d39..ce302f4 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,3 +1,8 @@ name = "telegram-mcp" main = "index.js" compatibility_date = "2026-01-01" + +# Required secrets (set via: wrangler secret put ): +# TELEGRAM_TOKEN — your bot token from @BotFather +# TELEGRAM_CHAT_ID — target chat/group ID +# MCP_SECRET — optional bearer token for auth