brightspace-mcp-server 3.11.4 → 3.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -247,13 +247,16 @@ async function finishDownload(response, originalFilename, downloadPath, customFi
|
|
|
247
247
|
export function registerDownloadFile(server, apiClient) {
|
|
248
248
|
server.registerTool("download_file", {
|
|
249
249
|
title: "Download File",
|
|
250
|
-
description: "Download a file from course content, assignment submissions, or an announcement's attachments. Use this when the user wants a file from Brightspace course content, dropbox submissions, or an announcement (newsId + fileId, from get_announcements). Two response modes: (1) INLINE (default — omit downloadPath): the file comes back directly in the tool response — extracted text for PDFs and Office documents, an image block for jpeg/png/gif/webp, or a short description for anything else — so it can be read immediately without touching any filesystem. This is the right choice in clients like Claude Desktop, whose analysis/sandbox tools cannot see a file the MCP server writes to its own host filesystem. (2) DISK (set downloadPath to an absolute path on the HOST filesystem the MCP server runs on): the file is saved there. Ask the user where to save it before using disk mode — never guess a directory. After identifying the file, suggest a clean readable filename (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and pass it as customFilename, or omit it to keep the original. If a content-topic download fails because the file isn't released yet, the response explains why when Brightspace's module/topic metadata supports it (not yet open, ended, locked, or hidden).",
|
|
250
|
+
description: "Download a file from course content, instructor assignment attachments (source: assignmentAttachment with folderId + fileId), assignment submissions, or an announcement's attachments. Use this when the user wants a file from Brightspace course content, dropbox submissions, or an announcement (newsId + fileId, from get_announcements). Two response modes: (1) INLINE (default — omit downloadPath): the file comes back directly in the tool response — extracted text for PDFs and Office documents, an image block for jpeg/png/gif/webp, or a short description for anything else — so it can be read immediately without touching any filesystem. This is the right choice in clients like Claude Desktop, whose analysis/sandbox tools cannot see a file the MCP server writes to its own host filesystem. (2) DISK (set downloadPath to an absolute path on the HOST filesystem the MCP server runs on): the file is saved there. Ask the user where to save it before using disk mode — never guess a directory. After identifying the file, suggest a clean readable filename (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and pass it as customFilename, or omit it to keep the original. If a content-topic download fails because the file isn't released yet, the response explains why when Brightspace's module/topic metadata supports it (not yet open, ended, locked, or hidden).",
|
|
251
251
|
inputSchema: DownloadFileSchema,
|
|
252
252
|
}, async (args) => {
|
|
253
253
|
try {
|
|
254
254
|
log("DEBUG", "download_file tool called", { args });
|
|
255
255
|
// Parse and validate input
|
|
256
|
-
const { courseId, topicId, folderId, newsId, fileId, downloadPath, customFilename } = DownloadFileSchema.parse(args);
|
|
256
|
+
const { courseId, topicId, folderId, newsId, fileId, downloadPath, customFilename, source } = DownloadFileSchema.parse(args);
|
|
257
|
+
if (source !== undefined && (topicId !== undefined || newsId !== undefined || folderId === undefined || fileId === undefined)) {
|
|
258
|
+
return errorResponse("source requires folderId and fileId and cannot be combined with topicId or newsId.");
|
|
259
|
+
}
|
|
257
260
|
// Validate courseId
|
|
258
261
|
validateContentId(courseId);
|
|
259
262
|
// downloadPath is optional: omitting it means "return the file
|
|
@@ -288,7 +291,7 @@ export function registerDownloadFile(server, apiClient) {
|
|
|
288
291
|
// Submission file download
|
|
289
292
|
validateContentId(folderId);
|
|
290
293
|
validateContentId(fileId);
|
|
291
|
-
return await downloadSubmissionFile(apiClient, courseId, folderId, fileId, downloadPath, customFilename);
|
|
294
|
+
return await (source === "assignmentAttachment" ? downloadAssignmentAttachment : downloadSubmissionFile)(apiClient, courseId, folderId, fileId, downloadPath, customFilename);
|
|
292
295
|
}
|
|
293
296
|
else if (newsId !== undefined && fileId !== undefined) {
|
|
294
297
|
// Announcement attachment download
|
|
@@ -305,6 +308,23 @@ export function registerDownloadFile(server, apiClient) {
|
|
|
305
308
|
}
|
|
306
309
|
});
|
|
307
310
|
}
|
|
311
|
+
/** Download an instructor-provided handout, independently of student submissions. */
|
|
312
|
+
async function downloadAssignmentAttachment(apiClient, courseId, folderId, fileId, downloadPath, customFilename) {
|
|
313
|
+
const raw = await apiClient.get(apiClient.le(courseId, "/dropbox/folders/"));
|
|
314
|
+
const folders = Array.isArray(raw) ? raw : raw?.Objects ?? [];
|
|
315
|
+
const folder = folders.find((candidate) => candidate.Id === folderId && candidate.IsHidden !== true);
|
|
316
|
+
if (!folder)
|
|
317
|
+
return errorResponse(`No visible assignment with id ${folderId} in course ${courseId}.`);
|
|
318
|
+
const attachments = folder.Attachments ?? [];
|
|
319
|
+
const file = attachments.find((candidate) => candidate.FileId === fileId);
|
|
320
|
+
if (!file)
|
|
321
|
+
return errorResponse(`File ID ${fileId} not found on this assignment. Available files: ${attachments.map((candidate) => `${candidate.FileName} (ID: ${candidate.FileId})`).join(", ")}`);
|
|
322
|
+
if (file.Size > maxFileSize(downloadPath))
|
|
323
|
+
return tooLargeResponse(file.Size, downloadPath);
|
|
324
|
+
const response = await apiClient.getRaw(apiClient.le(courseId, `/dropbox/folders/${folderId}/attachments/${fileId}`));
|
|
325
|
+
const filename = parseContentDispositionFilename(response.headers.get("Content-Disposition") ?? "") ?? file.FileName;
|
|
326
|
+
return finishDownload(response, filename, downloadPath, customFilename, "Assignment attachment");
|
|
327
|
+
}
|
|
308
328
|
/**
|
|
309
329
|
* Extract a filename from a Content-Disposition header.
|
|
310
330
|
*/
|
|
@@ -60,6 +60,7 @@ export function registerGetAssignmentFiles(server, apiClient, baseUrl) {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
const file = await readAttachment(apiClient, apiClient.le(courseId, `/dropbox/folders/${folderId}/attachments/${fileId}`), attachment, extractText, maxChars);
|
|
63
|
+
file.downloadArgs = { courseId, folderId, fileId, source: "assignmentAttachment" };
|
|
63
64
|
return toolResponse({
|
|
64
65
|
courseId,
|
|
65
66
|
folderId,
|
|
@@ -76,7 +77,10 @@ export function registerGetAssignmentFiles(server, apiClient, baseUrl) {
|
|
|
76
77
|
folderName: folder.Name,
|
|
77
78
|
dueDate: folder.DueDate,
|
|
78
79
|
url: baseUrl ? assignmentUrl(baseUrl, courseId, folder.Id) : null,
|
|
79
|
-
attachments: (folder.Attachments ?? []).map(
|
|
80
|
+
attachments: (folder.Attachments ?? []).map((attachment) => ({
|
|
81
|
+
...describeAttachment(attachment),
|
|
82
|
+
downloadArgs: { courseId, folderId: folder.Id, fileId: attachment.FileId, source: "assignmentAttachment" },
|
|
83
|
+
})),
|
|
80
84
|
}));
|
|
81
85
|
log("INFO", `get_assignment_files: ${withFiles.length} assignments with attachments in course ${courseId}`);
|
|
82
86
|
return toolResponse({
|
package/build/tools/schemas.js
CHANGED
|
@@ -62,14 +62,16 @@ export const GetClasslistEmailsSchema = z.object({
|
|
|
62
62
|
.describe("Course ID to get emails for."),
|
|
63
63
|
});
|
|
64
64
|
export const DownloadFileSchema = z.object({
|
|
65
|
+
source: z.enum(["submission", "assignmentAttachment"]).optional()
|
|
66
|
+
.describe("For folderId + fileId: assignmentAttachment downloads an instructor-provided handout from get_assignment_files; submission (default) downloads your own submitted file. Do not combine source with topicId or newsId."),
|
|
65
67
|
courseId: z.coerce.number().int().positive()
|
|
66
68
|
.describe("Course ID the file belongs to."),
|
|
67
69
|
topicId: z.coerce.number().int().positive().optional()
|
|
68
70
|
.describe("Content topic ID to download (for course content files)."),
|
|
69
71
|
folderId: z.coerce.number().int().positive().optional()
|
|
70
|
-
.describe("Dropbox folder ID
|
|
72
|
+
.describe("Dropbox folder ID. Downloads your submission by default; set source to assignmentAttachment for an instructor-provided handout."),
|
|
71
73
|
fileId: z.coerce.number().int().positive().optional()
|
|
72
|
-
.describe("Specific file ID within a dropbox submission, or
|
|
74
|
+
.describe("Specific file ID within a dropbox submission, instructor assignment attachment (with source: assignmentAttachment), or announcement attachment (with newsId)."),
|
|
73
75
|
newsId: z.coerce.number().int().positive().optional()
|
|
74
76
|
.describe("Announcement (news item) ID whose attachment to download. Requires fileId."),
|
|
75
77
|
downloadPath: z.string().min(1).optional()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.5",
|
|
4
4
|
"mcpName": "io.github.RohanMuppa/brightspace",
|
|
5
5
|
"description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
|
|
6
6
|
"type": "module",
|