UNPKG

digital-samba-mcp-server

Version:

Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API

959 lines 41.5 kB
/** * Webhook handler for Digital Samba MCP Server * * This module implements a comprehensive webhook system for the Digital Samba API, * allowing the MCP server to receive real-time events from Digital Samba * and propagate them to connected MCP clients. It provides: * * - Webhook endpoint registration and management * - Webhook event processing and validation * - Event signature verification for security * - Client notification for real-time updates * - Tools for managing webhooks through the MCP interface * * @module webhooks * @author Digital Samba Team * @version 0.1.0 */ // Node.js built-in modules import crypto from 'crypto'; // Local modules import { getApiKeyFromRequest } from './auth.js'; import { DigitalSambaApiClient } from './digital-samba-api.js'; import { ApiRequestError, ApiResponseError, AuthenticationError, ConfigurationError, ResourceNotFoundError, ValidationError } from './errors.js'; import logger from './logger.js'; // Define webhook event types export var WebhookEventType; (function (WebhookEventType) { // Room events WebhookEventType["ROOM_CREATED"] = "room.created"; WebhookEventType["ROOM_UPDATED"] = "room.updated"; WebhookEventType["ROOM_DELETED"] = "room.deleted"; // Session events WebhookEventType["SESSION_STARTED"] = "session.started"; WebhookEventType["SESSION_ENDED"] = "session.ended"; // Participant events WebhookEventType["PARTICIPANT_JOINED"] = "participant.joined"; WebhookEventType["PARTICIPANT_LEFT"] = "participant.left"; // Recording events WebhookEventType["RECORDING_STARTED"] = "recording.started"; WebhookEventType["RECORDING_STOPPED"] = "recording.stopped"; WebhookEventType["RECORDING_READY"] = "recording.ready"; // Chat events WebhookEventType["CHAT_MESSAGE"] = "chat.message"; // Poll events WebhookEventType["POLL_CREATED"] = "poll.created"; WebhookEventType["POLL_UPDATED"] = "poll.updated"; WebhookEventType["POLL_DELETED"] = "poll.deleted"; // Q&A events WebhookEventType["QUESTION_ASKED"] = "qa.question"; WebhookEventType["QUESTION_ANSWERED"] = "qa.answer"; })(WebhookEventType || (WebhookEventType = {})); /** * Webhook service for handling Digital Samba events * * This class provides the core functionality for receiving, processing, and * propagating webhook events from the Digital Samba API. It manages event * handlers, signature verification, and client notifications. * * @class * @example * const webhookService = new WebhookService(mcpServer, { * secret: process.env.WEBHOOK_SECRET, * endpoint: '/webhooks/digitalsamba' * }); * * // Register an event handler * webhookService.on(WebhookEventType.RECORDING_READY, async (payload) => { * logger.info('Recording is ready:', payload.data.id); * }); * * // Register the webhook endpoint with Express * webhookService.registerWebhookEndpoint(app); */ export class WebhookService { /** * Create a new webhook service * * @constructor * @param {McpServer} server - The MCP server instance for notifications * @param {WebhookConfig} config - Configuration for the webhook service * @param {string} [config.secret] - Secret for verifying webhook signatures * @param {string} config.endpoint - HTTP endpoint path for receiving webhooks */ constructor(server, config) { this.eventHandlers = new Map(); this.server = server; this.config = config; // Initialize event handlers map with empty arrays for each event type Object.values(WebhookEventType).forEach(eventType => { this.eventHandlers.set(eventType, []); }); } /** * Register the webhook endpoint with the Express app * * This method sets up the HTTP route that will receive webhook events from * the Digital Samba API and process them through the webhook service. * * @param {express.Application} app - Express application instance * @returns {void} */ registerWebhookEndpoint(app) { logger.info(`Registering webhook endpoint at ${this.config.endpoint}`); app.post(this.config.endpoint, this.handleWebhookRequest.bind(this)); } /** * Handle incoming webhook requests * * This method processes HTTP requests to the webhook endpoint. It verifies * signatures if a secret is configured, validates the payload, and triggers * event processing. * * @private * @param {Request} req - Express request object * @param {Response} res - Express response object * @returns {Promise<void>} */ async handleWebhookRequest(req, res) { try { logger.debug('Received webhook request', { contentLength: req.headers['content-length'], contentType: req.headers['content-type'] }); // Verify webhook signature if secret is configured if (this.config.secret) { const signature = req.headers['x-digitalsamba-signature']; if (!signature) { const error = new AuthenticationError('Missing webhook signature'); logger.warn(error.message); res.status(401).json({ error: error.message }); return; } if (!this.verifySignature(req, signature)) { const error = new AuthenticationError('Invalid webhook signature'); logger.warn(error.message); res.status(401).json({ error: error.message }); return; } } // Parse webhook payload const payload = req.body; if (!payload || !payload.event) { const error = new ValidationError('Invalid webhook payload', { validationErrors: { 'payload': 'Webhook payload must contain an event property' } }); logger.warn(error.message, { payload }); res.status(400).json({ error: error.message, details: error.validationErrors }); return; } logger.info(`Processing webhook event: ${payload.event}`, { event: payload.event, timestamp: payload.timestamp }); // Process the webhook event await this.processWebhookEvent(payload); // Respond with success res.status(200).json({ status: 'ok' }); } catch (error) { logger.error('Error processing webhook', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); // Respond with appropriate status code based on error type if (!res.headersSent) { if (error instanceof AuthenticationError) { res.status(401).json({ error: error.message }); } else if (error instanceof ValidationError) { res.status(400).json({ error: error.message, details: error.validationErrors }); } else if (error instanceof ApiResponseError) { res.status(error.statusCode).json({ error: error.message, apiError: error.apiErrorMessage, details: error.apiErrorData }); } else if (error instanceof ResourceNotFoundError) { res.status(404).json({ error: error.message, resourceType: error.resourceType, resourceId: error.resourceId }); } else { // Default server error res.status(500).json({ error: 'Internal server error' }); } } } } /** * Verify the webhook signature * * Uses HMAC-SHA256 to verify that the webhook was sent by Digital Samba * and that the payload hasn't been tampered with. * * @private * @param {Request} req - Express request object * @param {string} signature - Signature from the X-DigitalSamba-Signature header * @returns {boolean} True if signature is valid, false otherwise */ verifySignature(req, signature) { if (!this.config.secret) { return true; // No verification if no secret configured } try { // Create HMAC using the secret const hmac = crypto.createHmac('sha256', this.config.secret); // Update with request body (as string) const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body); hmac.update(body); // Get the digest const calculatedSignature = hmac.digest('hex'); // Compare with the provided signature return crypto.timingSafeEqual(Buffer.from(calculatedSignature, 'hex'), Buffer.from(signature, 'hex')); } catch (error) { logger.error('Error verifying webhook signature', { error: error instanceof Error ? error.message : String(error) }); throw new AuthenticationError(`Error verifying webhook signature: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined }); } } /** * Process a webhook event * * Executes all registered handlers for the event type and notifies MCP clients. * * @private * @param {WebhookPayload} payload - The webhook event payload * @returns {Promise<void>} */ async processWebhookEvent(payload) { const { event } = payload; const handlers = this.eventHandlers.get(event); if (!handlers || handlers.length === 0) { logger.debug(`No handlers registered for event: ${event}`); return; } logger.debug(`Executing ${handlers.length} handlers for event: ${event}`); // Execute all registered handlers const promises = handlers.map(handler => handler(payload)); await Promise.all(promises); // Notify MCP clients about the event await this.notifyMcpClients(payload); } /** * Register a handler for a specific event type * * Allows custom logic to be executed when specific webhook events are received. * * @public * @param {WebhookEventType} event - The event type to listen for * @param {WebhookEventHandler} handler - Function to execute when event occurs * @returns {void} * @example * webhookService.on(WebhookEventType.RECORDING_READY, async (payload) => { * logger.info(`Recording ${payload.data.id} is ready for viewing`); * // Custom logic for when a recording is ready * }); */ on(event, handler) { const handlers = this.eventHandlers.get(event) || []; handlers.push(handler); this.eventHandlers.set(event, handlers); logger.debug(`Registered handler for event: ${event}`); } /** * Notify MCP clients about a webhook event * * Sends a notification to all connected MCP clients about the webhook event. * This allows clients to receive real-time updates about Digital Samba events. * * @private * @param {WebhookPayload} payload - The webhook event payload * @returns {Promise<void>} */ async notifyMcpClients(payload) { try { logger.debug(`Notifying MCP clients about event: ${payload.event}`); // Create notification message based on event type const notification = this.createNotificationForEvent(payload); // Send notification to all connected clients // Note: Different versions of MCP SDK might have different notification methods try { // Use a type assertion to access potentially private properties const mcpServer = this.server; // Try different methods for sending notifications based on SDK version if (typeof mcpServer.notification === 'function') { // Newer SDK version await mcpServer.notification({ method: 'digitalsambaEvent', params: notification }); logger.debug('Sent notification using server.notification'); } else if (mcpServer.server && typeof mcpServer.server.notification === 'function') { // Older SDK version await mcpServer.server.notification({ method: 'digitalsambaEvent', params: notification }); logger.debug('Sent notification using server.server.notification'); } else { // No notification method found const error = new ConfigurationError('Unable to send notification: notification method not found'); logger.warn(error.message); throw error; } } catch (notifyError) { logger.error('Error during notification send', { error: notifyError instanceof Error ? notifyError.message : String(notifyError) }); throw new ApiRequestError(`Failed to send notification: ${notifyError instanceof Error ? notifyError.message : String(notifyError)}`, { cause: notifyError instanceof Error ? notifyError : undefined }); } logger.debug('Notification process completed'); } catch (error) { logger.error('Error in notifyMcpClients', { error: error instanceof Error ? error.message : String(error) }); // Re-throw the error to be handled by the caller throw error; } } /** * Create a notification object based on the event type * * Formats the webhook payload into a structured notification object * based on the event type, extracting relevant fields for each event category. * * @private * @param {WebhookPayload} payload - The webhook event payload * @returns {Object} Formatted notification object */ createNotificationForEvent(payload) { const { event, data } = payload; // Create a base notification with common fields const notification = { event, timestamp: payload.timestamp, data: {} }; // Add specific fields based on event type switch (event) { case WebhookEventType.ROOM_CREATED: case WebhookEventType.ROOM_UPDATED: case WebhookEventType.ROOM_DELETED: notification.data = { roomId: data.id, room: data }; break; case WebhookEventType.SESSION_STARTED: case WebhookEventType.SESSION_ENDED: notification.data = { sessionId: data.id, roomId: data.room_id, session: data }; break; case WebhookEventType.PARTICIPANT_JOINED: case WebhookEventType.PARTICIPANT_LEFT: notification.data = { participantId: data.id, roomId: data.room_id, sessionId: data.session_id, participant: data }; break; case WebhookEventType.RECORDING_STARTED: case WebhookEventType.RECORDING_STOPPED: case WebhookEventType.RECORDING_READY: notification.data = { recordingId: data.id, roomId: data.room_id, sessionId: data.session_id, recording: data }; break; case WebhookEventType.CHAT_MESSAGE: notification.data = { messageId: data.id, roomId: data.room_id, participantId: data.participant_id, message: data }; break; case WebhookEventType.POLL_CREATED: case WebhookEventType.POLL_UPDATED: case WebhookEventType.POLL_DELETED: notification.data = { pollId: data.id, roomId: data.room_id, poll: data }; break; case WebhookEventType.QUESTION_ASKED: case WebhookEventType.QUESTION_ANSWERED: notification.data = { questionId: data.id, roomId: data.room_id, participantId: data.participant_id, question: data }; break; default: // For unknown events, include the entire data payload notification.data = data; } return notification; } /** * Register or update a webhook with Digital Samba API * * Creates a new webhook registration or updates an existing one with * the Digital Samba API, specifying which events to subscribe to. * * @public * @param {string} apiKey - Digital Samba API key * @param {string} apiBaseUrl - Base URL for the Digital Samba API * @param {string} webhookUrl - URL where webhook events should be sent * @param {WebhookEventType[]} [eventTypes] - Event types to subscribe to (defaults to all) * @returns {Promise<void>} * @throws Will throw an error if the API request fails */ async registerWebhook(apiKey, apiBaseUrl, webhookUrl, eventTypes = Object.values(WebhookEventType)) { try { // Validate input parameters if (!apiKey) { throw new AuthenticationError('API key is required for webhook registration'); } if (!webhookUrl) { throw new ValidationError('Webhook URL is required', { validationErrors: { webhookUrl: 'Webhook URL cannot be empty' } }); } if (!apiBaseUrl) { throw new ValidationError('API base URL is required', { validationErrors: { apiBaseUrl: 'API base URL cannot be empty' } }); } logger.info('Registering webhook with Digital Samba API', { webhookUrl, eventCount: eventTypes.length }); // Create API client with the provided key const client = new DigitalSambaApiClient(apiKey, apiBaseUrl); try { // Get existing webhooks const webhooks = await client.listWebhooks(); // Check if a webhook already exists for this URL const existingWebhook = webhooks.data.find(webhook => webhook.endpoint === webhookUrl); if (existingWebhook) { // Update existing webhook logger.info('Updating existing webhook', { webhookId: existingWebhook.id }); await client.updateWebhook(existingWebhook.id, { endpoint: webhookUrl, events: eventTypes, name: 'MCP Server Webhook' }); } else { // Create new webhook logger.info('Creating new webhook'); await client.createWebhook({ endpoint: webhookUrl, events: eventTypes, name: 'MCP Server Webhook' }); } logger.info('Webhook registration successful'); } catch (error) { // Handle API-specific errors if (error instanceof Error) { if ('statusCode' in error) { const statusCode = error.statusCode; const errorMessage = error.message; if (statusCode === 401 || statusCode === 403) { throw new AuthenticationError(`Authentication failed during webhook registration: ${errorMessage}`, { cause: error }); } if (statusCode === 404) { throw new ResourceNotFoundError('Resource not found during webhook registration', { resourceId: 'webhook', resourceType: 'webhook', cause: error }); } throw new ApiResponseError(`API error during webhook registration`, { statusCode, apiErrorMessage: errorMessage, cause: error }); } } // Default error handling throw new ApiRequestError(`Error registering webhook: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined }); } } catch (error) { logger.error('Error registering webhook', { error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Delete a webhook with Digital Samba API * * Deletes a webhook registration with the Digital Samba API based on the URL. * * @public * @param {string} apiKey - Digital Samba API key * @param {string} apiBaseUrl - Base URL for the Digital Samba API * @param {string} webhookUrl - URL of the webhook to delete * @returns {Promise<void>} * @throws Will throw an error if the API request fails */ async deleteWebhook(apiKey, apiBaseUrl, webhookUrl) { try { // Validate input parameters if (!apiKey) { throw new AuthenticationError('API key is required for webhook deletion'); } if (!webhookUrl) { throw new ValidationError('Webhook URL is required', { validationErrors: { webhookUrl: 'Webhook URL cannot be empty' } }); } if (!apiBaseUrl) { throw new ValidationError('API base URL is required', { validationErrors: { apiBaseUrl: 'API base URL cannot be empty' } }); } logger.info('Deleting webhook from Digital Samba API', { webhookUrl }); // Create API client with the provided key const client = new DigitalSambaApiClient(apiKey, apiBaseUrl); try { // Get existing webhooks const webhooks = await client.listWebhooks(); // Find webhook for this URL const existingWebhook = webhooks.data.find(webhook => webhook.endpoint === webhookUrl); if (existingWebhook) { // Delete the webhook logger.info('Deleting webhook', { webhookId: existingWebhook.id }); await client.deleteWebhook(existingWebhook.id); logger.info('Webhook deletion successful'); } else { logger.info('No webhook found for the specified URL'); throw new ResourceNotFoundError('No webhook found for the specified URL', { resourceId: webhookUrl, resourceType: 'webhook' }); } } catch (error) { // Handle API-specific errors if (error instanceof ResourceNotFoundError) { // Re-throw ResourceNotFoundError throw error; } if (error instanceof Error) { if ('statusCode' in error) { const statusCode = error.statusCode; const errorMessage = error.message; if (statusCode === 401 || statusCode === 403) { throw new AuthenticationError(`Authentication failed during webhook deletion: ${errorMessage}`, { cause: error }); } if (statusCode === 404) { throw new ResourceNotFoundError('Webhook not found', { resourceId: webhookUrl, resourceType: 'webhook', cause: error }); } throw new ApiResponseError(`API error during webhook deletion`, { statusCode, apiErrorMessage: errorMessage, cause: error }); } } // Default error handling throw new ApiRequestError(`Error deleting webhook: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined }); } } catch (error) { logger.error('Error deleting webhook', { error: error instanceof Error ? error.message : String(error) }); throw error; } } } /** * Create webhook handling tools for MCP server * * Sets up MCP tools for managing webhooks, including registration, deletion, * and listing of webhooks and available event types. These tools allow clients * to interact with the webhook system through the MCP interface. * * @param {McpServer} server - The MCP server instance * @param {WebhookService} webhookService - The webhook service instance * @param {string} apiBaseUrl - Base URL for the Digital Samba API * @returns {void} */ export function setupWebhookTools(server, webhookService, apiBaseUrl) { // Tool for registering a webhook server.tool('register-webhook', { webhookUrl: { type: 'string', description: 'The URL where Digital Samba will send webhook events' }, events: { type: 'array', items: { type: 'string', enum: Object.values(WebhookEventType) }, description: 'Event types to subscribe to', optional: true } }, async (params, request) => { try { // Verify request is defined and properly initialized if (!request) { throw new Error('Request object is undefined'); } // Ensure request has sessionId property // Note: We previously tried to get sessionId from context, but that's not available // in the current version of McpServer - using only request.sessionId now // Get API key from the context const apiKey = getApiKeyFromRequest(request); const { webhookUrl, events } = params; if (!apiKey) { throw new AuthenticationError('API key is required for webhook registration. Please provide a Bearer token in the Authorization header or set DIGITAL_SAMBA_API_KEY environment variable.'); } if (!webhookUrl) { throw new ValidationError('Webhook URL is required', { validationErrors: { webhookUrl: 'Webhook URL cannot be empty' } }); } // Register the webhook await webhookService.registerWebhook(apiKey, apiBaseUrl, webhookUrl, events); return { content: [{ type: 'text', text: `Webhook registered successfully at ${webhookUrl}`, }], }; } catch (error) { logger.error('Error in register-webhook tool', { error: error instanceof Error ? error.message : String(error) }); let errorResponse = { text: `Error registering webhook: ${error instanceof Error ? error.message : String(error)}`, isError: true }; // Customize error response based on error type if (error instanceof AuthenticationError) { errorResponse.text = error.message; } else if (error instanceof ValidationError) { errorResponse.text = error.message; // If we have validation errors, add them to the message if (Object.keys(error.validationErrors).length > 0) { errorResponse.text += '\n\nValidation errors:'; for (const [field, message] of Object.entries(error.validationErrors)) { errorResponse.text += `\n- ${field}: ${message}`; } } } else if (error instanceof ResourceNotFoundError) { errorResponse.text = `Resource not found: ${error.resourceType} with ID ${error.resourceId} does not exist`; } else if (error instanceof ApiResponseError) { // Handle specific status codes with user-friendly messages if (error.statusCode === 404) { errorResponse.text = `Resource not found: ${error.apiErrorMessage}`; } else if (error.statusCode === 403) { errorResponse.text = `Authentication error: Insufficient permissions to register webhook`; } else if (error.statusCode === 400) { errorResponse.text = `Validation error: ${error.apiErrorMessage}`; } else { errorResponse.text = `API error (${error.statusCode}): ${error.apiErrorMessage}`; } } return { content: [ { type: 'text', text: errorResponse.text, }, ], isError: errorResponse.isError, }; } }); // Tool for deleting a webhook server.tool('delete-webhook', { webhookUrl: { type: 'string', description: 'The URL of the webhook to delete' } }, async (params, request) => { try { // Verify request is defined and properly initialized if (!request) { throw new Error('Request object is undefined'); } // Ensure request has sessionId property // Note: We previously tried to get sessionId from context, but that's not available // in the current version of McpServer - using only request.sessionId now // Get API key from the context const apiKey = getApiKeyFromRequest(request); const { webhookUrl } = params; if (!apiKey) { throw new AuthenticationError('API key is required for webhook deletion. Please provide a Bearer token in the Authorization header or set DIGITAL_SAMBA_API_KEY environment variable.'); } if (!webhookUrl) { throw new ValidationError('Webhook URL is required', { validationErrors: { webhookUrl: 'Webhook URL cannot be empty' } }); } // Delete the webhook await webhookService.deleteWebhook(apiKey, apiBaseUrl, webhookUrl); return { content: [{ type: 'text', text: `Webhook at ${webhookUrl} deleted successfully.`, }], }; } catch (error) { logger.error('Error in delete-webhook tool', { error: error instanceof Error ? error.message : String(error) }); let errorResponse = { text: `Error deleting webhook: ${error instanceof Error ? error.message : String(error)}`, isError: true }; // Customize error response based on error type if (error instanceof AuthenticationError) { errorResponse.text = error.message; } else if (error instanceof ValidationError) { errorResponse.text = error.message; // If we have validation errors, add them to the message if (Object.keys(error.validationErrors).length > 0) { errorResponse.text += '\n\nValidation errors:'; for (const [field, message] of Object.entries(error.validationErrors)) { errorResponse.text += `\n- ${field}: ${message}`; } } } else if (error instanceof ResourceNotFoundError) { errorResponse.text = `Webhook not found: No webhook exists at ${params.webhookUrl}`; } else if (error instanceof ApiResponseError) { // Handle specific status codes with user-friendly messages if (error.statusCode === 404) { errorResponse.text = `Webhook not found: ${error.apiErrorMessage}`; } else if (error.statusCode === 403) { errorResponse.text = `Authentication error: Insufficient permissions to delete webhook`; } else { errorResponse.text = `API error (${error.statusCode}): ${error.apiErrorMessage}`; } } return { content: [ { type: 'text', text: errorResponse.text, }, ], isError: errorResponse.isError, }; } }); // Tool for listing webhooks server.tool('list-webhooks', {}, async (params, request) => { try { // Verify request is defined and properly initialized if (!request) { throw new Error('Request object is undefined'); } // Ensure request has sessionId property // Note: We previously tried to get sessionId from context, but that's not available // in the current version of McpServer - using only request.sessionId now // Get API key from the context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { throw new AuthenticationError('API key is required for listing webhooks. Please provide a Bearer token in the Authorization header or set DIGITAL_SAMBA_API_KEY environment variable.'); } // Create API client with the provided key const client = new DigitalSambaApiClient(apiKey, apiBaseUrl); // Get webhooks const webhooks = await client.listWebhooks(); if (webhooks.data.length === 0) { return { content: [{ type: 'text', text: 'No webhooks found.', }], }; } const webhookList = webhooks.data.map(webhook => { return `ID: ${webhook.id}\nURL: ${webhook.endpoint}\nName: ${webhook.name || 'N/A'}\nEvents: ${webhook.events?.join(', ') || 'All'}\nCreated: ${webhook.created_at}\n`; }).join('\n---\n\n'); return { content: [{ type: 'text', text: `Found ${webhooks.data.length} webhooks:\n\n${webhookList}`, }], }; } catch (error) { logger.error('Error in list-webhooks tool', { error: error instanceof Error ? error.message : String(error) }); let errorResponse = { text: `Error listing webhooks: ${error instanceof Error ? error.message : String(error)}`, isError: true }; // Customize error response based on error type if (error instanceof AuthenticationError) { errorResponse.text = error.message; } else if (error instanceof ApiResponseError) { if (error.statusCode === 403) { errorResponse.text = `Authentication error: Insufficient permissions to list webhooks`; } else { errorResponse.text = `API error (${error.statusCode}): ${error.apiErrorMessage}`; } } return { content: [ { type: 'text', text: errorResponse.text, }, ], isError: errorResponse.isError, }; } }); // Tool for listing available webhook event types server.tool('list-webhook-events', {}, async (params, request) => { try { // Verify request is defined and properly initialized if (!request) { throw new Error('Request object is undefined'); } // Ensure request has sessionId property // Note: We previously tried to get sessionId from context, but that's not available // in the current version of McpServer - using only request.sessionId now // Get API key from the context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { throw new AuthenticationError('API key is required for listing webhook events. Please provide a Bearer token in the Authorization header or set DIGITAL_SAMBA_API_KEY environment variable.'); } // Create API client with the provided key const client = new DigitalSambaApiClient(apiKey, apiBaseUrl); // Get available event types const events = await client.listWebhookEvents(); // Group events by category const categories = {}; events.forEach(event => { const category = event.split('.')[0]; if (!categories[category]) { categories[category] = []; } categories[category].push(event); }); // Format the output let output = 'Available webhook event types:\n\n'; Object.entries(categories).forEach(([category, eventList]) => { output += `${category.toUpperCase()} EVENTS:\n`; eventList.forEach(event => { output += `- ${event}\n`; }); output += '\n'; }); return { content: [{ type: 'text', text: output, }], }; } catch (error) { logger.error('Error in list-webhook-events tool', { error: error instanceof Error ? error.message : String(error) }); let errorResponse = { text: `Error listing webhook events: ${error instanceof Error ? error.message : String(error)}`, isError: true }; // Customize error response based on error type if (error instanceof AuthenticationError) { errorResponse.text = error.message; } else if (error instanceof ApiResponseError) { if (error.statusCode === 403) { errorResponse.text = `Authentication error: Insufficient permissions to list webhook events`; } else { errorResponse.text = `API error (${error.statusCode}): ${error.apiErrorMessage}`; } } return { content: [ { type: 'text', text: errorResponse.text, }, ], isError: errorResponse.isError, }; } }); } export default WebhookService; //# sourceMappingURL=webhooks.js.map