Skip to main content
Glama

download_file

Download files from Brightspace course content or assignment submissions to your local computer. Specify where to save and optionally rename files for better organization.

Instructions

Download a file from course content or assignment submissions to a local directory. Use this when the user wants to download, save, or get a file from Brightspace course content or dropbox submissions. IMPORTANT: You MUST ask the user where they want to save the file before calling this tool. Never guess or assume a download directory. After identifying the file to download, suggest a clean readable filename to the user (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and ask if they'd like to rename it. Pass their preferred name as customFilename, or omit it to keep the original.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
courseIdYesCourse ID the file belongs to.
topicIdNoContent topic ID to download (for course content files).
folderIdNoDropbox folder ID (for submission/feedback file downloads).
fileIdNoSpecific file ID within a dropbox submission.
downloadPathYesAbsolute path to the directory where the file should be saved.
customFilenameNoCustom filename for the downloaded file (include extension). If not provided, uses the original filename from Brightspace.

Implementation Reference

  • Main tool registration and handler function. Registers the download_file tool with MCP, validates inputs, and routes to either content file download (via topicId) or submission file download (via folderId + fileId). Includes path validation and error handling.
    export function registerDownloadFile( server: McpServer, apiClient: D2LApiClient ): void { server.registerTool( "download_file", { title: "Download File", description: "Download a file from course content or assignment submissions to a local directory. Use this when the user wants to download, save, or get a file from Brightspace course content or dropbox submissions. IMPORTANT: You MUST ask the user where they want to save the file before calling this tool. Never guess or assume a download directory. After identifying the file to download, suggest a clean readable filename to the user (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and ask if they'd like to rename it. Pass their preferred name as customFilename, or omit it to keep the original.", inputSchema: DownloadFileSchema, }, async (args: any) => { try { log("DEBUG", "download_file tool called", { args }); // Parse and validate input const { courseId, topicId, folderId, fileId, downloadPath, customFilename } = DownloadFileSchema.parse(args); // Validate courseId validateContentId(courseId); // Validate download path is absolute if (!path.isAbsolute(downloadPath)) { return errorResponse( "Download path must be an absolute path (e.g., /Users/username/Downloads on Mac or C:\\Users\\username\\Downloads on Windows)" ); } // Validate download directory exists and is a directory try { const stats = await fs.stat(downloadPath); if (!stats.isDirectory()) { return errorResponse( `Download path is not a directory: ${downloadPath}` ); } } catch (error: any) { if (error?.code === "ENOENT") { return errorResponse( `Download directory does not exist: ${downloadPath}` ); } throw error; } // Determine download source if (topicId !== undefined) { // Content file download validateContentId(topicId); return await downloadContentFile( apiClient, courseId, topicId, downloadPath, customFilename ); } else if (folderId !== undefined && fileId !== undefined) { // Submission file download validateContentId(folderId); validateContentId(fileId); return await downloadSubmissionFile( apiClient, courseId, folderId, fileId, downloadPath, customFilename ); } else { return errorResponse( "Either topicId (for content files) or both folderId and fileId (for submission files) must be provided" ); } } catch (error) { return sanitizeError(error); } } ); }
  • downloadContentFile helper function. Downloads course content files using topicId, fetches file from D2L API using getRaw, validates Content-Length header, extracts filename from Content-Disposition, and uses secureDownload for safe file writing.
    async function downloadContentFile( apiClient: D2LApiClient, courseId: number, topicId: number, downloadPath: string, customFilename?: string ): Promise<any> { log( "INFO", `Downloading content file: courseId=${courseId}, topicId=${topicId}` ); // Build download URL using D2L API path helper const apiPath = apiClient.le(courseId, `/content/topics/${topicId}/file`); // Fetch file using getRaw (returns Response object, not parsed JSON) const response = await apiClient.getRaw(apiPath); // Check Content-Length BEFORE downloading body (prevent memory exhaustion) const contentLength = parseInt( response.headers.get("Content-Length") ?? "0", 10 ); if (contentLength > MAX_FILE_SIZE) { return errorResponse( `File too large (${Math.round(contentLength / 1024 / 1024)}MB). Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB` ); } // Get filename from Content-Disposition header const disposition = response.headers.get("Content-Disposition") ?? ""; let filename = "download"; const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (match?.[1]) { filename = match[1].replace(/['"]/g, ""); } log("DEBUG", `Content-Disposition filename: ${filename}`); // Download body as buffer const buffer = Buffer.from(await response.arrayBuffer()); // Double-check actual size if (buffer.length > MAX_FILE_SIZE) { return errorResponse( `File too large (${Math.round(buffer.length / 1024 / 1024)}MB). Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB` ); } // Use custom filename if provided, otherwise use Content-Disposition filename const originalFilename = filename; const effectiveFilename = customFilename || filename; // Use secureDownload for path traversal prevention, file type validation, and conflict resolution const result = await secureDownload({ targetDir: downloadPath, filename: effectiveFilename, data: buffer, }); log( "INFO", `File downloaded successfully: ${result.path} (${result.size} bytes, ${result.mime})` ); return toolResponse({ success: true, filePath: result.path, fileSize: result.size, mimeType: result.mime, originalFilename, message: `File downloaded successfully to ${result.path}`, }); }
  • downloadSubmissionFile helper function. Downloads dropbox/submission files using folderId and fileId. Fetches submission metadata first to validate file exists and get original filename, then downloads the file and uses secureDownload for safe file writing.
    async function downloadSubmissionFile( apiClient: D2LApiClient, courseId: number, folderId: number, fileId: number, downloadPath: string, customFilename?: string ): Promise<any> { log( "INFO", `Downloading submission file: courseId=${courseId}, folderId=${folderId}, fileId=${fileId}` ); // D2L API pattern for submission file downloads: // GET /d2l/api/le/(version)/(orgUnitId)/dropbox/folders/(folderId)/submissions/mysubmissions/ // Then find the file by fileId and construct its download URL // First, fetch the submission to get file metadata const submissionsPath = apiClient.le( courseId, `/dropbox/folders/${folderId}/submissions/mysubmissions/` ); interface DropboxSubmission { Id: number; Files: Array<{ FileId: number; FileName: string; Size: number; }>; } const submissions = await apiClient.get<DropboxSubmission[]>(submissionsPath); if (!submissions || submissions.length === 0) { return errorResponse( "No submissions found for this assignment. Upload a submission first." ); } // Find the file in the submission const submission = submissions[0]; const file = submission.Files.find((f) => f.FileId === fileId); if (!file) { return errorResponse( `File ID ${fileId} not found in submission. Available files: ${submission.Files.map((f) => `${f.FileName} (ID: ${f.FileId})`).join(", ")}` ); } // Check file size before downloading if (file.Size > MAX_FILE_SIZE) { return errorResponse( `File too large (${Math.round(file.Size / 1024 / 1024)}MB). Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB` ); } // D2L file download URL pattern for submission files // GET /d2l/api/le/(version)/(orgUnitId)/dropbox/folders/(folderId)/submissions/(submissionId)/files/(fileId)/download const downloadApiPath = apiClient.le( courseId, `/dropbox/folders/${folderId}/submissions/${submission.Id}/files/${fileId}/download` ); // Fetch file const response = await apiClient.getRaw(downloadApiPath); // Download body as buffer const buffer = Buffer.from(await response.arrayBuffer()); // Double-check actual size if (buffer.length > MAX_FILE_SIZE) { return errorResponse( `File too large (${Math.round(buffer.length / 1024 / 1024)}MB). Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB` ); } // Use custom filename if provided, otherwise use original submission filename const originalFilename = file.FileName; const effectiveFilename = customFilename || file.FileName; // Use secureDownload for path traversal prevention, file type validation, and conflict resolution const result = await secureDownload({ targetDir: downloadPath, filename: effectiveFilename, data: buffer, }); log( "INFO", `Submission file downloaded successfully: ${result.path} (${result.size} bytes, ${result.mime})` ); return toolResponse({ success: true, filePath: result.path, fileSize: result.size, mimeType: result.mime, originalFilename, message: `File downloaded successfully to ${result.path}`, }); }
  • DownloadFileSchema - Zod input validation schema for the download_file tool. Defines required courseId, optional topicId (for content), optional folderId/fileId (for submissions), required downloadPath, and optional customFilename.
    export const DownloadFileSchema = z.object({ courseId: z.number().int().positive() .describe("Course ID the file belongs to."), topicId: z.number().int().positive().optional() .describe("Content topic ID to download (for course content files)."), folderId: z.number().int().positive().optional() .describe("Dropbox folder ID (for submission/feedback file downloads)."), fileId: z.number().int().positive().optional() .describe("Specific file ID within a dropbox submission."), downloadPath: z.string().min(1) .describe("Absolute path to the directory where the file should be saved."), customFilename: z.string().optional() .describe("Custom filename for the downloaded file (include extension). If not provided, uses the original filename from Brightspace."), });
  • src/index.ts:24-24 (registration)
    Import of registerDownloadFile function from tools index, and registration call at line 171 where the tool is registered with the MCP server instance.
    registerDownloadFile,

Latest Blog Posts

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/RohanMuppa/brightspace-mcp-server'

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