Skip to main content
Glama

Download File

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,
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively communicates that this is a write operation (file download to local system), includes important safety constraints (must ask user for download path, never guess), and provides guidance on filename handling. However, it doesn't mention potential errors, rate limits, or authentication requirements that might be relevant.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the core purpose. Every sentence adds value: the first states what the tool does, the second specifies when to use it, and the remaining sentences provide crucial procedural guidance. Some minor redundancy exists between 'download, save, or get a file' but overall it's efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 6-parameter tool with no annotations and no output schema, the description does well by covering the tool's purpose, usage context, and important behavioral constraints. It explains parameter relationships and provides filename guidance. The main gap is lack of information about return values or error conditions, but given the tool's complexity, it's reasonably complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the baseline is 3. The description adds meaningful context by explaining the distinction between topicId (for course content files) and folderId/fileId (for dropbox submissions), and provides practical guidance on customFilename usage. However, it doesn't fully explain the mutual exclusivity or relationships between these parameters beyond what's implied.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('download a file'), identifies the source resources ('from course content or assignment submissions'), and specifies the destination ('to a local directory'). It distinguishes this tool from sibling tools like 'get_course_content' or 'get_assignments' by focusing on file retrieval rather than metadata listing.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use this tool ('when the user wants to download, save, or get a file from Brightspace course content or dropbox submissions') and includes important procedural instructions ('MUST ask the user where they want to save the file before calling this tool'). It also distinguishes between content files (using topicId) and submission files (using folderId/fileId), though it doesn't explicitly name alternative tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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