digital-samba-mcp-server
Version:
Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API
777 lines • 34.8 kB
JavaScript
/**
* Digital Samba MCP Server - Recording Functionality
*
* This module implements resources and tools for managing Digital Samba room recordings.
* It provides capabilities for listing, retrieving, and managing recordings
* through the MCP interface, exposing the Digital Samba recording API to MCP clients.
*
* Features include:
* - Listing all recordings (standard and archived)
* - Retrieving specific recording details
* - Generating download links
* - Archiving and unarchiving recordings
* - Deleting recordings
*
* @module recordings
* @author Digital Samba Team
* @version 1.0.0
*/
// External dependencies
import { z } from 'zod';
// MCP SDK imports
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
// Local modules
import { getApiKeyFromRequest } from './auth.js';
import { DigitalSambaApiClient } from './digital-samba-api.js';
import { ApiResponseError, AuthenticationError, ResourceNotFoundError, ValidationError } from './errors.js';
import logger from './logger.js';
/**
* Set up recording resources and tools for the MCP server
*
* This function registers all recording-related resources and tools with the MCP server.
* It creates resources for listing and retrieving recordings, as well as tools for
* managing recording operations.
*
* @param {McpServer} server - The MCP server instance
* @param {string} apiUrl - Base URL for the Digital Samba API
* @returns {void}
*
* @example
* // Register recording functionality with the MCP server
* setupRecordingFunctionality(mcpServer, 'https://api.digitalsamba.com/api/v1');
*/
export function setupRecordingFunctionality(server, apiUrl) {
// -------------------------------------------------------------------
// Resources
// -------------------------------------------------------------------
// Resource for listing all recordings
server.resource('recordings', new ResourceTemplate('digitalsamba://recordings', { list: undefined }), async (uri, _params, request) => {
logger.info('Listing all recordings');
// Get API key from session context
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
throw new AuthenticationError('No API key found. Please include an Authorization header with a Bearer token.');
}
// Create API client
logger.debug('Creating API client using context API key');
const client = new DigitalSambaApiClient(undefined, apiUrl);
try {
// Get recordings from API
const response = await client.listRecordings();
const recordings = response.data || [];
logger.debug(`Found ${recordings.length} recordings`);
// Format recordings as resource contents
const contents = recordings.map(recording => ({
uri: `digitalsamba://recordings/${recording.id}`,
text: JSON.stringify(recording, null, 2),
}));
return { contents };
}
catch (error) {
logger.error('Error fetching recordings', { error: error instanceof Error ? error.message : String(error) });
// Handle specific error types
if (error instanceof Error) {
// Handle authentication errors
if (error.message.includes('401') || error.message.includes('unauthorized')) {
throw new AuthenticationError('Authentication failed. Please check your API key.');
}
// Rethrow the original error with more context if it's not a specific case
throw new ApiResponseError(`Failed to fetch recordings`, {
statusCode: error.message.includes('404') ? 404 : 500,
apiErrorMessage: error.message
});
}
throw error;
}
});
// Resource for getting a specific recording
server.resource('recording', new ResourceTemplate('digitalsamba://recordings/{recordingId}', { list: undefined }), async (uri, params, request) => {
const { recordingId } = params;
if (!recordingId) {
throw new ValidationError('Recording ID is required.', {
validationErrors: { recordingId: 'This field is required' }
});
}
logger.info('Getting recording details', { recordingId });
// Get API key from session context
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
throw new AuthenticationError('No API key found. Please include an Authorization header with a Bearer token.');
}
// Create API client
logger.debug('Creating API client using context API key');
const client = new DigitalSambaApiClient(undefined, apiUrl);
try {
// Get recording from API
const recording = await client.getRecording(recordingId);
// Format recording as resource content
const content = {
uri: uri.href,
text: JSON.stringify(recording, null, 2),
};
return { contents: [content] };
}
catch (error) {
logger.error('Error fetching recording', {
recordingId,
error: error instanceof Error ? error.message : String(error)
});
// Handle specific error types
if (error instanceof Error) {
// Handle 404 errors specifically for better user experience
if (error.message.includes('404') || error.message.includes('not found')) {
throw new ResourceNotFoundError(`Recording with ID ${recordingId} not found`, {
resourceId: recordingId,
resourceType: 'recording'
});
}
// Handle authentication errors
if (error.message.includes('401') || error.message.includes('unauthorized')) {
throw new AuthenticationError('Authentication failed. Please check your API key.');
}
// Rethrow the original error with more context if it's not a specific case
throw new ApiResponseError(`Failed to fetch recording details`, {
statusCode: error.message.includes('404') ? 404 : 500,
apiErrorMessage: error.message
});
}
throw error;
}
});
// Resource for listing recordings for a specific room
server.resource('room-recordings', new ResourceTemplate('digitalsamba://rooms/{roomId}/recordings', { list: undefined }), async (uri, params, request) => {
const { roomId } = params;
if (!roomId) {
throw new ValidationError('Room ID is required.', {
validationErrors: { roomId: 'This field is required' }
});
}
logger.info('Listing recordings for room', { roomId });
// Get API key from session context
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
throw new AuthenticationError('No API key found. Please include an Authorization header with a Bearer token.');
}
// Create API client
logger.debug('Creating API client using context API key');
const client = new DigitalSambaApiClient(undefined, apiUrl);
try {
// Get recordings from API for the specific room
const response = await client.listRecordings({ room_id: roomId });
const recordings = response.data || [];
logger.debug(`Found ${recordings.length} recordings for room ${roomId}`);
// Format recordings as resource contents
const contents = recordings.map(recording => ({
uri: `digitalsamba://recordings/${recording.id}`,
text: JSON.stringify(recording, null, 2),
}));
return { contents };
}
catch (error) {
logger.error('Error fetching room recordings', {
roomId,
error: error instanceof Error ? error.message : String(error)
});
// Handle specific error types
if (error instanceof Error) {
// Handle 404 errors specifically for better user experience
if (error.message.includes('404') || error.message.includes('not found')) {
throw new ResourceNotFoundError(`Room with ID ${roomId} not found`, {
resourceId: roomId,
resourceType: 'room'
});
}
// Handle authentication errors
if (error.message.includes('401') || error.message.includes('unauthorized')) {
throw new AuthenticationError('Authentication failed. Please check your API key.');
}
// Rethrow the original error with more context if it's not a specific case
throw new ApiResponseError(`Failed to fetch recordings for room ${roomId}`, {
statusCode: error.message.includes('404') ? 404 : 500,
apiErrorMessage: error.message
});
}
throw error;
}
});
// Resource for listing archived recordings
server.resource('archived-recordings', new ResourceTemplate('digitalsamba://recordings/archived', { list: undefined }), async (uri, _params, request) => {
logger.info('Listing archived recordings');
// Get API key from session context
const apiKey = getApiKeyFromRequest(request);
if (!apiKey) {
throw new AuthenticationError('No API key found. Please include an Authorization header with a Bearer token.');
}
// Create API client
logger.debug('Creating API client using context API key');
const client = new DigitalSambaApiClient(undefined, apiUrl);
try {
// Get archived recordings from API
const response = await client.listArchivedRecordings();
const recordings = response.data || [];
logger.debug(`Found ${recordings.length} archived recordings`);
// Format recordings as resource contents
const contents = recordings.map(recording => ({
uri: `digitalsamba://recordings/archived/${recording.id}`,
text: JSON.stringify(recording, null, 2),
}));
return { contents };
}
catch (error) {
logger.error('Error fetching archived recordings', {
error: error instanceof Error ? error.message : String(error)
});
// Handle specific error types
if (error instanceof Error) {
// Handle authentication errors
if (error.message.includes('401') || error.message.includes('unauthorized')) {
throw new AuthenticationError('Authentication failed. Please check your API key.');
}
// Rethrow the original error with more context if it's not a specific case
throw new ApiResponseError(`Failed to fetch archived recordings`, {
statusCode: error.message.includes('404') ? 404 : 500,
apiErrorMessage: error.message
});
}
throw error;
}
});
// -------------------------------------------------------------------
// Tools
// -------------------------------------------------------------------
// 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(undefined, 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(undefined, 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(undefined, 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(undefined, 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(undefined, 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(undefined, 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 functionality set up successfully');
}
//# sourceMappingURL=recordings.js.map