UNPKG

@digitalsamba/embedded-api-mcp-server

Version:

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

742 lines 29.7 kB
/** * Digital Samba MCP Server - Recording Management Tools * * This module implements tools for managing Digital Samba room recordings. * It provides MCP tools for recording operations like retrieval, deletion, * archiving, and download link generation. * * Tools provided: * - get-recordings: List recordings with filtering * - delete-recording: Delete a recording * - get-recording: Get specific recording details * - get-recording-download-link: Generate download links * - archive-recording: Archive a recording * - unarchive-recording: Unarchive a recording * * @module tools/recording-management * @author Digital Samba Team * @version 1.0.0 */ // External dependencies import { z } from "zod"; // Local modules import { getApiKeyFromRequest } from "../../auth.js"; import { DigitalSambaApiClient } from "../../digital-samba-api.js"; import logger from "../../logger.js"; /** * Set up recording management tools for the MCP server * * This function registers all recording-related tools with the MCP server. * Tools are action endpoints that allow manipulation of recording data. * * @param {McpServer} server - The MCP server instance * @param {string} apiUrl - Base URL for the Digital Samba API * @returns {void} */ export function setupRecordingTools(server, apiUrl) { // Tool for starting recording in a room server.tool("start-recording", { roomId: z.string(), }, async (params, request) => { const { roomId } = params; // Validate required parameters if (!roomId || roomId.trim() === "") { return { content: [ { type: "text", text: "Room ID is required to start recording.", }, ], isError: true, }; } logger.info("Starting recording", { roomId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { await client.startRecording(roomId); return { content: [ { type: "text", text: `Recording started successfully in room ${roomId}`, }, ], }; } catch (error) { logger.error("Error starting recording", { roomId, error: error instanceof Error ? error.message : String(error), }); const errorMessage = error instanceof Error ? error.message : String(error); let displayMessage = `Error starting recording: ${errorMessage}`; // Transform specific error messages to match test expectations if (errorMessage.includes("Room not found") || errorMessage.includes("404")) { displayMessage = `Room with ID ${roomId} not found`; } else if (errorMessage.includes("Already recording")) { displayMessage = `Recording already in progress for room ${roomId}`; } return { content: [ { type: "text", text: displayMessage, }, ], isError: true, }; } }); // Tool for stopping recording in a room server.tool("stop-recording", { roomId: z.string(), }, async (params, request) => { const { roomId } = params; logger.info("Stopping recording", { roomId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { await client.stopRecording(roomId); return { content: [ { type: "text", text: `Recording stopped successfully in room ${roomId}`, }, ], }; } catch (error) { logger.error("Error stopping recording", { roomId, error: error instanceof Error ? error.message : String(error), }); return { content: [ { type: "text", text: `Error stopping recording: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Tool for retrieving all recordings server.tool("get-recordings", { roomId: z.string().optional(), status: z.enum(["IN_PROGRESS", "PENDING_CONVERSION", "READY"]).optional(), limit: z.number().min(1).max(100).optional(), offset: z.number().min(0).optional(), archived: z.boolean().optional(), }, async (params, request) => { const { roomId, status, limit, offset, archived } = params; logger.info("Retrieving recordings", { roomId, status, archived: archived ? true : false, }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { let recordings; if (archived) { // Get archived recordings const response = await client.listArchivedRecordings({ room_id: roomId, limit, offset, }); recordings = response.data || []; logger.debug(`Found ${recordings.length} archived recordings`); } else { // Get standard recordings const response = await client.listRecordings({ room_id: roomId, status, limit, offset, }); recordings = response.data || []; logger.debug(`Found ${recordings.length} recordings`); } // Format response let responseText = `Found ${recordings.length} recording(s)`; if (roomId) responseText += ` for room ${roomId}`; if (status) responseText += ` with status ${status}`; if (archived) responseText += ` (archived)`; responseText += ":\\n\\n"; // Add a formatted list of recordings if (recordings.length === 0) { responseText += "No recordings found."; } else { responseText += recordings .map((recording, index) => { let recordingInfo = `${index + 1}. ID: ${recording.id}\\n`; recordingInfo += ` Status: ${recording.status}\\n`; recordingInfo += ` Room: ${recording.room_id}\\n`; recordingInfo += ` Created: ${new Date(recording.created_at).toLocaleString()}\\n`; if (recording.duration) { recordingInfo += ` Duration: ${recording.duration} seconds\\n`; } return recordingInfo; }) .join("\\n"); // Add instruction for getting details responseText += "\\n\\nTo get details for a specific recording, use the get-recording-download-link tool with the recording ID."; } return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { logger.error("Error retrieving recordings", { roomId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error retrieving recordings. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { if (roomId) { errorMessage = `Room with ID ${roomId} not found.`; } else { errorMessage = `Requested resource not found.`; } } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to access these recordings."; } // Use the original error message for any other cases else { errorMessage = `Error retrieving recordings: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); // Tool for deleting a recording server.tool("delete-recording", { recordingId: z.string(), }, async (params, request) => { const { recordingId } = params; if (!recordingId) { return { content: [ { type: "text", text: "Recording ID is required.", }, ], isError: true, }; } logger.info("Deleting recording", { recordingId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { // Delete recording await client.deleteRecording(recordingId); logger.info("Recording deleted successfully", { recordingId }); return { content: [ { type: "text", text: `Recording ${recordingId} deleted successfully.`, }, ], }; } catch (error) { logger.error("Error deleting recording", { recordingId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error deleting recording. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { errorMessage = `Recording with ID ${recordingId} not found.`; } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to delete this recording."; } // Use the original error message for any other cases else { errorMessage = `Error deleting recording: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); // Tool for getting a specific recording server.tool("get-recording", { recordingId: z.string(), }, async (params, request) => { const { recordingId } = params; if (!recordingId) { return { content: [ { type: "text", text: "Recording ID is required.", }, ], isError: true, }; } logger.info("Getting recording details", { recordingId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { // Get recording const recording = await client.getRecording(recordingId); logger.info("Recording details retrieved successfully", { recordingId, }); // Format detailed response for human readability let responseText = `Recording Details for ID: ${recording.id}\\n\\n`; responseText += `Status: ${recording.status}\\n`; responseText += `Room ID: ${recording.room_id}\\n`; if (recording.name) { responseText += `Name: ${recording.name}\\n`; } if (recording.friendly_url) { responseText += `Friendly URL: ${recording.friendly_url}\\n`; } if (recording.participant_name) { responseText += `Recorded by: ${recording.participant_name}\\n`; } if (recording.duration) { const minutes = Math.floor(recording.duration / 60); const seconds = recording.duration % 60; responseText += `Duration: ${minutes}m ${seconds}s\\n`; } responseText += `Created: ${new Date(recording.created_at).toLocaleString()}\\n`; responseText += `Updated: ${new Date(recording.updated_at).toLocaleString()}\\n\\n`; // Add download link instructions if (recording.status === "READY") { responseText += `To get a download link, use the get-recording-download-link tool with this recording ID.`; } else { responseText += `This recording is not ready for download yet (status: ${recording.status}).`; } return { content: [ { type: "text", text: responseText, }, ], }; } catch (error) { logger.error("Error getting recording details", { recordingId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error getting recording details. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { errorMessage = `Recording with ID ${recordingId} not found.`; } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to view this recording."; } // Use the original error message for any other cases else { errorMessage = `Error getting recording details: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); // Tool for getting a recording download link server.tool("get-recording-download-link", { recordingId: z.string(), validForMinutes: z.number().min(1).max(1440).optional(), }, async (params, request) => { const { recordingId, validForMinutes } = params; if (!recordingId) { return { content: [ { type: "text", text: "Recording ID is required.", }, ], isError: true, }; } logger.info("Getting download link for recording", { recordingId, validForMinutes, }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { // Get download link const downloadLink = await client.getRecordingDownloadLink(recordingId, validForMinutes); logger.info("Download link generated successfully", { recordingId }); return { content: [ { type: "text", text: `Download link generated successfully!\\n\\n${JSON.stringify(downloadLink, null, 2)}`, }, ], }; } catch (error) { logger.error("Error generating download link", { recordingId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error generating download link. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { errorMessage = `Recording with ID ${recordingId} not found.`; } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to download this recording."; } // Handle recording not ready errors else if (error.message.toLowerCase().includes("not ready") || error.message.toLowerCase().includes("in progress") || error.message.toLowerCase().includes("pending")) { errorMessage = `Recording is not ready for download yet. Current status may be 'IN_PROGRESS' or 'PENDING_CONVERSION'.`; } // Use the original error message for any other cases else { errorMessage = `Error generating download link: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); // Tool for archiving a recording server.tool("archive-recording", { recordingId: z.string(), }, async (params, request) => { const { recordingId } = params; if (!recordingId) { return { content: [ { type: "text", text: "Recording ID is required.", }, ], isError: true, }; } logger.info("Archiving recording", { recordingId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { // Archive recording await client.archiveRecording(recordingId); logger.info("Recording archived successfully", { recordingId }); return { content: [ { type: "text", text: `Recording ${recordingId} archived successfully.`, }, ], }; } catch (error) { logger.error("Error archiving recording", { recordingId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error archiving recording. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { errorMessage = `Recording with ID ${recordingId} not found.`; } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to archive this recording."; } // Handle already archived errors else if (error.message.toLowerCase().includes("already archived")) { errorMessage = `Recording ${recordingId} is already archived.`; } // Use the original error message for any other cases else { errorMessage = `Error archiving recording: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); // Tool for unarchiving a recording server.tool("unarchive-recording", { recordingId: z.string(), }, async (params, request) => { const { recordingId } = params; if (!recordingId) { return { content: [ { type: "text", text: "Recording ID is required.", }, ], isError: true, }; } logger.info("Unarchiving recording", { recordingId }); // Get API key from session context const apiKey = getApiKeyFromRequest(request); if (!apiKey) { return { content: [ { type: "text", text: "No API key found. Please include an Authorization header with a Bearer token.", }, ], isError: true, }; } // Create API client logger.debug("Creating API client using context API key"); const client = new DigitalSambaApiClient(apiKey, apiUrl); try { // Unarchive recording await client.unarchiveRecording(recordingId); logger.info("Recording unarchived successfully", { recordingId }); return { content: [ { type: "text", text: `Recording ${recordingId} unarchived successfully.`, }, ], }; } catch (error) { logger.error("Error unarchiving recording", { recordingId, error: error instanceof Error ? error.message : String(error), }); // Format error message based on error type let errorMessage = "Error unarchiving recording. Please try again."; if (error instanceof Error) { // Handle 404 errors specifically for better user experience if (error.message.includes("404") || error.message.includes("not found")) { errorMessage = `Recording with ID ${recordingId} not found in the archive.`; } // Handle authentication errors else if (error.message.includes("401") || error.message.includes("unauthorized")) { errorMessage = "Authentication failed. Please check your API key."; } // Handle permission errors else if (error.message.includes("403") || error.message.includes("forbidden")) { errorMessage = "You do not have permission to unarchive this recording."; } // Handle not archived errors else if (error.message.toLowerCase().includes("not archived")) { errorMessage = `Recording ${recordingId} is not currently archived.`; } // Use the original error message for any other cases else { errorMessage = `Error unarchiving recording: ${error.message}`; } } return { content: [{ type: "text", text: errorMessage }], isError: true, }; } }); logger.info("Recording management tools set up successfully"); } //# sourceMappingURL=index.js.map