UNPKG

@sinch/mcp

Version:

Sinch MCP server

227 lines 9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startWebhookServer = exports.startWebhookListener = exports.openNgrokTunnel = exports.registerCapabilities = exports.parseArgs = exports.instantiateMcpServer = void 0; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const ngrok_1 = __importDefault(require("@ngrok/ngrok")); const express_1 = __importDefault(require("express")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const http_1 = __importDefault(require("http")); const better_sqlite3_1 = __importDefault(require("better-sqlite3")); const sdk_core_1 = require("@sinch/sdk-core"); const prompts_1 = require("./prompts"); const verification_1 = require("./tools/verification"); const conversation_1 = require("./tools/conversation"); const voice_1 = require("./tools/voice"); const email_1 = require("./tools/email"); const configuration_1 = require("./tools/configuration"); const package_json_1 = require("../package.json"); let webhookId; let webhookPort; const instantiateMcpServer = () => { return new mcp_js_1.McpServer({ name: 'Sinch', version: package_json_1.version, capabilities: { resources: {}, tools: {}, prompts: {} } }); }; exports.instantiateMcpServer = instantiateMcpServer; const parseArgs = (args) => { const args1 = args.slice(2); return args1.includes('--tags') ? args1[args1.indexOf('--tags') + 1].split(',') : []; }; exports.parseArgs = parseArgs; const registerCapabilities = (server, tags) => { if (tags.length === 0) tags.push('all'); // Register the prompts (0, prompts_1.registerPrompts)(server, tags); // Register the tools (0, verification_1.registerVerificationTools)(server, tags); (0, conversation_1.registerConversationTools)(server, tags); (0, voice_1.registerVoiceTools)(server, tags); (0, email_1.registerEmailTools)(server, tags); (0, configuration_1.registerConfigurationTools)(server); }; exports.registerCapabilities = registerCapabilities; const openNgrokTunnel = async () => { const listener = await ngrok_1.default.forward({ port: webhookPort, authtoken: process.env.NGROK_AUTH_TOKEN, }); const conversationWebhookUrl = listener.url(); if (!conversationWebhookUrl) { throw new Error('Failed to obtain ngrok URL'); } return conversationWebhookUrl; }; exports.openNgrokTunnel = openNgrokTunnel; const startWebhookListener = async () => { // We must have found a port to use for the webhook events receiver if (!webhookPort) return; let conversationWebhookUrl; try { conversationWebhookUrl = await (0, exports.openNgrokTunnel)(); } catch (error) { console.error('Failed to open ngrok tunnel:', error); return; } const sinchClient = new sdk_core_1.SinchClient({ projectId: process.env.CONVERSATION_PROJECT_ID, keyId: process.env.CONVERSATION_KEY_ID, keySecret: process.env.CONVERSATION_KEY_SECRET, }); let createWebhookResponse; try { createWebhookResponse = await sinchClient.conversation.webhooks.create({ webhookCreateRequestBody: { app_id: process.env.CONVERSATION_APP_ID, target: conversationWebhookUrl, triggers: [ 'MESSAGE_DELIVERY', 'MESSAGE_SUBMIT' ], target_type: 'HTTP' } }); console.info('Webhook created successfully:', createWebhookResponse.id); } catch (error) { console.error('Failed to create webhook:', error); return; } webhookId = createWebhookResponse.id; return; }; exports.startWebhookListener = startWebhookListener; const deleteWebhook = async (webhookId) => { const sinchClient = new sdk_core_1.SinchClient({ projectId: process.env.CONVERSATION_PROJECT_ID, keyId: process.env.CONVERSATION_KEY_ID, keySecret: process.env.CONVERSATION_KEY_SECRET, }); try { await sinchClient.conversation.webhooks.delete({ webhook_id: webhookId }); } catch (error) { console.error(`Failed to delete webhook with ID ${webhookId}:`, error); } }; const startWebhookServer = async () => { // The ngrok tunnel is for the conversation webhook only => check if the environment variables are set if (!process.env.CONVERSATION_PROJECT_ID || !process.env.CONVERSATION_KEY_ID || !process.env.CONVERSATION_KEY_SECRET || !process.env.CONVERSATION_APP_ID) return; // The user must have also set the NGROK_AUTH_TOKEN environment variable if (!process.env.NGROK_AUTH_TOKEN) return; const app = (0, express_1.default)(); app.use(express_1.default.json()); const dbDir = path_1.default.join(__dirname, './data'); const dbPath = path_1.default.join(dbDir, 'webhooks.db'); if (!fs_1.default.existsSync(dbDir)) { fs_1.default.mkdirSync(dbDir, { recursive: true }); } const db = new better_sqlite3_1.default(dbPath); db.prepare(` CREATE TABLE IF NOT EXISTS webhooks_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, app_id TEXT NOT NULL, event_time DATETIME NOT NULL, message_id TEXT NOT NULL, channel_identity TEXT NOT NULL, status TEXT, reason TEXT, submitted_message TEXT ) `).run(); const insertStmt = db.prepare(` INSERT INTO webhooks_events ( type, app_id, event_time, message_id, channel_identity, status, reason, submitted_message ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); app.post('/', (req, res) => { const conversationCallbackWebhook = new sdk_core_1.ConversationCallbackWebhooks(''); const event = conversationCallbackWebhook.parseEvent(req.body); const appId = event.app_id; const eventTime = event.event_time; let messageId = ''; let channelIdentity = ''; let status = null; let reason = null; let submittedMessage = null; switch (event.trigger) { case 'MESSAGE_DELIVERY': messageId = event.message_delivery_report.message_id; channelIdentity = JSON.stringify(event.message_delivery_report.channel_identity); status = event.message_delivery_report.status; reason = JSON.stringify(event.message_delivery_report.reason); break; case 'MESSAGE_SUBMIT': messageId = event.message_submit_notification.message_id; channelIdentity = JSON.stringify(event.message_submit_notification.channel_identity); submittedMessage = JSON.stringify(event.message_submit_notification.submitted_message); break; default: // Should never go there as we only register MESSAGE_DELIVERY and MESSAGE_SUBMIT triggers return; } insertStmt.run(event.trigger, appId, eventTime, messageId, channelIdentity, status, reason, submittedMessage); res.status(200).send(); }); const httpServer = http_1.default.createServer(app); httpServer.listen(0, async () => { const address = httpServer.address(); if (typeof address === 'object' && address !== null) { webhookPort = address.port; console.info(`Webhook server listening on port ${webhookPort}`); } // Once the server is ready, create a webhook in the conversation app await (0, exports.startWebhookListener)(); }); const shutdown = () => { console.info('Shutting down webhook server...'); httpServer.close(async () => { console.info('HTTP server closed.'); console.info('Deleting webhook in the conversation app...'); if (webhookId) { try { await deleteWebhook(webhookId); console.info(`Webhook ${webhookId} cleanup completed.`); } catch (error) { console.error(`Webhook ${webhookId} cleanup failed:`, error); } } db.close(); console.info('Database connection closed.'); try { fs_1.default.unlinkSync(dbPath); console.info('Database file deleted.'); } catch (err) { console.error('Failed to delete DB file:', err.message); } process.exit(0); }); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); }; exports.startWebhookServer = startWebhookServer; //# sourceMappingURL=server.js.map