Skip to main content
Glama

AI-Powered MCP Server

by larryfang
index.js16.1 kB
require('dotenv').config(); const express = require('express'); const { OpenAI } = require('openai'); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/doc', express.static(path.join(__dirname, 'doc'))); app.use('/conversations', express.static(path.join(__dirname, 'conversations'))); const PORT = process.env.PORT || 3000; // 🔐 Auth header const authHeader = 'Basic ' + Buffer.from( `${process.env.MESSAGE_API_KEY}:${process.env.MESSAGE_API_SECRET}` ).toString('base64'); // 📡 Axios client for MessageMedia const messageAPI = axios.create({ baseURL: process.env.MESSAGE_BASE_URL, // e.g. https://api.messagemedia.com headers: { Authorization: authHeader, 'Content-Type': 'application/json', Accept: 'application/json', ...(process.env.MESSAGE_SUB_ACCOUNT_ID && { Account: process.env.MESSAGE_SUB_ACCOUNT_ID }) } }); // 📨 /send endpoint — send a real SMS app.post('/send', async (req, res) => { const { messages } = req.body; if (!messages || !Array.isArray(messages) || messages.length === 0) { return res.status(400).json({ error: 'Request body must include a non-empty messages array.' }); } try { const response = await messageAPI.post('/v1/messages', { messages }); res.status(200).json({ message: 'SMS sent successfully!', response: response.data }); } catch (err) { console.error('❌ Error sending SMS:', err.response?.data || err.message); res.status(500).json({ error: 'Failed to send SMS', details: err.response?.data || err.message }); } }); app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.get('/version', (req, res) => { res.json({ name: "message-media-mcp-server", version: "1.0.0", description: "AI-native MCP server for SMS context and delivery", timestamp: new Date().toISOString() }); }); // 🗃️ Webhook storage const webhookLogPath = path.join(__dirname, 'webhook-log.json'); app.post('/context', async (req, res) => { console.error("context being called"); const { phone_number, use_live_data = true } = req.body; if (!phone_number) { return res.status(400).json({ error: 'phone_number is required' }); } try { const endDate = new Date().toISOString(); const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); if (use_live_data) { const payload = { start_date: startDate, end_date: endDate }; const response = await messageAPI.post(`/v2-preview/reporting/messages/detail`, payload); const messages = response.data.messages || []; const filtered = messages.filter(msg => msg.source_address === phone_number || msg.destination_address === phone_number ); // MO = user replies, MT = assistant messages const replies = filtered.filter( m => m.direction === 'MO' && m.source_address === phone_number && m.content?.trim() ); const deliveries = filtered.filter( m => m.direction === 'MT' && m.destination_address === phone_number ); replies.forEach((reply, index) => { console.error(`📩 Reply [${index}]:`, { content: reply.content, timestamp: reply.timestamp, direction: reply.direction, source: reply.source_address, destination: reply.destination_address, message_id: reply.message_id, }); }); const lastReply = replies.at(0); const lastDelivery = deliveries.at(0); const context = []; if (replies.length > 0) { context.push({ type: "list", label: "Recent Replies", value: replies.slice(-100).map(r => ({ content: r.content || '[no content]', date_received: r.timestamp })) }); } if (deliveries.length > 0) { context.push({ type: "list", label: "Recent Delivery Reports", value: deliveries.slice(-100).map(d => ({ status: d.status_description || d.status || 'unknown', message_id: d.message_id, date_received: d.timestamp })) }); } const lastReplyText = lastReply?.content || "None"; const lastDeliveryStatus = lastDelivery?.status_description || lastDelivery?.status || "N/A"; const summary = `${phone_number} has ${replies.length} reply(ies) and ${deliveries.length} delivery report(s). Last reply: "${lastReplyText}" Last delivery status: ${lastDeliveryStatus}.`; const prompt_context = `Phone ${phone_number}: Last status "${lastDeliveryStatus}", Last reply: "${lastReplyText}"`; const prompt_guidance = { usage: "Use this context to understand the customer's SMS interaction history.", examples: [ `This customer replied "${lastReplyText}" after receiving 2 messages.`, `${phone_number} has not replied yet, last delivery status was ${lastDeliveryStatus}.` ] }; return res.json({ summary, prompt_context, context, prompt_guidance }); } // Legacy fallback mode (webhook-log.json) const logFile = path.join(__dirname, 'webhook-log.json'); if (!fs.existsSync(logFile)) { return res.status(200).json({ summary: 'No logs yet.', context: [] }); } const logs = JSON.parse(fs.readFileSync(logFile, 'utf8')); const filtered = logs.filter(entry => entry.source_number === phone_number); const replies = filtered.filter(e => e.type === 'reply'); const deliveries = filtered.filter(e => e.type === 'delivery'); const lastReply = replies.at(0); const lastDelivery = deliveries.at(0); const context = []; if (replies.length > 0) { context.push({ type: "list", label: "Recent Replies", value: replies.slice(-100).map(r => ({ content: r.reply_content || r.content || '[no content]', date_received: r.date_received || r.received_at })) }); } if (deliveries.length > 0) { context.push({ type: "list", label: "Recent Delivery Reports", value: deliveries.slice(-100).map(d => ({ status: d.status, date_received: d.date_received, message_id: d.message_id })) }); } const lastReplyText = lastReply?.reply_content || lastReply?.content || "None"; const lastDeliveryStatus = lastDelivery?.status || "N/A"; const summary = `${phone_number} has ${replies.length} reply(ies) and ${deliveries.length} delivery report(s). Last reply: "${lastReplyText}" Last delivery status: ${lastDeliveryStatus}.`; const prompt_context = `Phone ${phone_number}: Last status "${lastDeliveryStatus}", Last reply: "${lastReplyText}"`; const prompt_guidance = { usage: "Use this context to understand the customer's SMS interaction history.", examples: [ `This customer replied "${lastReplyText}" after receiving 2 messages.`, `${phone_number} has not replied yet, last delivery status was ${lastDeliveryStatus}.` ] }; return res.json({ summary, prompt_context, context, prompt_guidance }); } catch (err) { console.error("Error in /context:", err.response?.data || err.message || err); return res.status(500).json({ error: "Failed to generate context" }); } }); function saveConversation(phone, role, message, intent) { if (!phone) return; const dir = path.join(__dirname, 'conversations'); const filePath = path.join(dir, `${phone}.json`); try { if (!fs.existsSync(dir)) fs.mkdirSync(dir); let history = []; if (fs.existsSync(filePath)) { history = JSON.parse(fs.readFileSync(filePath, 'utf8')); } history.push({ timestamp: new Date().toISOString(), role, message, intent: intent || null }); fs.writeFileSync(filePath, JSON.stringify(history, null, 2)); } catch (err) { console.error('Failed to save conversation:', err.message); } } function logWebhookEvent(type, data) { const file = path.join(__dirname, 'webhook-log.json'); try { let log = []; if (fs.existsSync(file)) { log = JSON.parse(fs.readFileSync(file, 'utf8')); } log.push({ type, timestamp: new Date().toISOString(), ...data }); fs.writeFileSync(file, JSON.stringify(log, null, 2)); } catch (err) { console.error('❌ Failed to log webhook event:', err.message); } // ALSO: Save message content to conversation log if applicable const phone = data?.source_number; if (phone && (data.reply_content || data.auto_reply)) { if (data.reply_content) { saveConversation(phone, 'user', data.reply_content); } if (data.auto_reply) { saveConversation(phone, 'assistant', data.auto_reply, data.intent); } } } // 📬 /webhook/delivery — log delivery status updates app.post('/webhook/delivery', (req, res) => { console.error('📬 Delivery webhook received:', req.body); try { logWebhookEvent('delivery', req.body); res.status(200).send('OK'); } catch (err) { console.error('Error logging delivery:', err); res.status(500).send('Failed to log delivery report'); } }); // 📩 /webhook/reply — log inbound SMS replies app.post('/webhook/reply', async (req, res) => { try { const source_number = req.body.source_address; const reply_content = req.body.reply_msg; console.error("📩 Incoming SMS:", { from: source_number, text: reply_content }); // 1. Generate the GPT reply const gptResponse = await openai.chat.completions.create({ model: "gpt-4-0613", messages: [ { role: "system", content: "You're a helpful and friendly SMS assistant. Reply casually and clearly to customers." }, { role: "user", content: reply_content } ] }); const reply = gptResponse.choices[0].message.content.trim(); console.error("🤖 GPT reply:", reply); // 2. Classify intent const classify = await openai.chat.completions.create({ model: "gpt-4-0613", messages: [ { role: "system", content: "Classify the user's message into one of: inquiry_shipping, complaint_product, confirm_receipt, cancel_order, other. Respond with just the label." }, { role: "user", content: reply_content } ] }); const intent = classify.choices[0].message.content.trim(); console.error("🔎 GPT intent:", intent); // 3. Send the reply back via your own /send API await axios.post(`${process.env.MCP_SERVER_URL}/send`, { messages: [ { destination_number: source_number, content: reply, format: "SMS", delivery_report: true } ] }); res.status(200).send("Auto-reply sent via GPT"); } catch (err) { console.error("❌ Error auto-replying to SMS:", err.message); res.status(500).send("Failed to auto-reply"); } }); app.get('/dashboard', async (req, res) => { try { const now = new Date(); const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // past 7 days const response = await messageAPI.post('/v2-preview/reporting/messages/detail', { start_date: start.toISOString(), end_date: now.toISOString() }); const messages = response.data.messages || []; const rows = messages.map(msg => { const isReply = msg.direction === 'MO'; const isDelivery = msg.status?.toLowerCase() === 'delivered' || msg.status; return ` <tr style="background: ${isReply ? '#e6f7ff' : '#f9f9f9'}"> <td>${msg.direction}</td> <td>${msg.source_address || '-'}</td> <td>${msg.destination_address || '-'}</td> <td>${msg.content || '-'}</td> <td>${msg.status || '-'}</td> <td>${msg.message_id || '-'}</td> <td>${msg.received || msg.timestamp || '-'}</td> </tr> `; }).join(''); const html = ` <html> <head> <title>📊 Live SMS Dashboard</title> <style> body { font-family: sans-serif; padding: 1rem; } table { border-collapse: collapse; width: 100%; } th, td { padding: 8px 12px; border: 1px solid #ccc; text-align: left; } th { background: #f0f0f0; } </style> </head> <body> <h2>📊 Live SMS Log Dashboard (Last 7 Days)</h2> <table> <tr> <th>Direction</th> <th>From</th> <th>To</th> <th>Content</th> <th>Status</th> <th>Message ID</th> <th>Timestamp</th> </tr> ${rows} </table> </body> </html> `; res.send(html); } catch (err) { console.error("❌ Error in /dashboard:", err.message || err); res.status(500).send("Failed to load dashboard"); } }); app.get('/report', async (req, res) => { try { const endDate = new Date().toISOString(); const startDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString(); const response = await messageAPI.post('/v2-preview/reporting/messages/detail', { start_date: startDate, end_date: endDate }); const messages = response.data.messages || []; // Parse and aggregate const dailyStats = {}; for (const msg of messages) { const date = new Date(msg.timestamp).toISOString().split('T')[0]; if (!dailyStats[date]) { dailyStats[date] = { date, Inbound: 0, Outbound: 0 }; } if (msg.direction === 'MO') { dailyStats[date].Inbound += 1; } else if (msg.direction === 'MT') { dailyStats[date].Outbound += 1; } } const summary = Object.values(dailyStats).sort((a, b) => new Date(a.date) - new Date(b.date)); res.json({ summary }); } catch (err) { console.error("Error in /report:", err.response?.data || err.message || err); res.status(500).json({ error: 'Failed to generate report' }); } }); // MCP schema metadata app.get('/meta', (req, res) => { res.json({ service: "messagemedia.sms", description: "SMS history and delivery context for phone numbers", context_types: ["phone_number"], capabilities: ["send_sms", "get_sms_context", "get_sms_report"], version: "1.0.0", protocols: { http: { endpoints: { context: "/context", meta: "/meta", schema: "/function-schema" } } }, rate_limits: { requests_per_minute: 60 // if you have any } }); }); // OpenAI-compatible function schema app.get('/function-schema', (req, res) => { res.json([ { name: "get_sms_context", description: "Fetch SMS context for a phone number", parameters: { type: "object", properties: { phone_number: { type: "string", description: "Phone number to fetch context for (E.164)" } }, required: ["phone_number"] } }, { name: "send_sms", description: "Send an SMS to a user", parameters: { type: "object", properties: { destination_number: { type: "string", description: "Recipient's phone number in E.164 format" }, content: { type: "string", description: "Text message content" } }, required: ["destination_number", "content"] } } ]); }); // 🔊 Start server app.listen(PORT, () => { console.error(`✅ MCP server listening on http://localhost:${PORT}`); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/larryfang/sms-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server