brightspace-mcp-server 3.9.16 → 3.9.17
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.
- package/README.md +1 -0
- package/build/index.js +10 -3
- package/build/tools/download-dropbox-submission-file.js +131 -0
- package/build/tools/get-dropbox-feedback.js +83 -0
- package/build/tools/get-dropbox-folders.js +86 -0
- package/build/tools/get-dropbox-submissions.js +163 -0
- package/build/tools/get-dropbox-user-submissions.js +102 -0
- package/build/tools/get-rubrics-for-object.js +94 -0
- package/build/tools/index.js +7 -0
- package/build/tools/schemas.js +61 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
|
|
|
71
71
|
| Discussions | "Summarize the latest posts in the final project thread" |
|
|
72
72
|
| Lecture videos | "What did the professor say about pinch-off in Tuesday's recording?" (Kaltura and YouTube) |
|
|
73
73
|
| Calendar | "When is my midterm?" · "Is lab cancelled on Thursday?" |
|
|
74
|
+
| For instructors and TAs | "Which students haven't submitted Lab 4 yet?" · "What feedback did I leave on this student's homework?" · "Download that student's submitted PDF" (students see a clear "instructor access required" note) |
|
|
74
75
|
|
|
75
76
|
## Prompts
|
|
76
77
|
|
package/build/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startUpdateChecks } from "./utils/update-checker.js";
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { dirname, resolve } from "node:path";
|
|
19
|
-
import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetCalendarEvents, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentRubric, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, registerGetClasslistEmails, registerGetRoster, registerGetMyGroups, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, registerSearchCourse, } from "./tools/index.js";
|
|
19
|
+
import { registerGetMyCourses, registerGetUpcomingDueDates, registerGetCalendarEvents, registerGetMyGrades, registerGetAnnouncements, registerGetAssignments, registerGetAssignmentRubric, registerGetAssignmentFiles, registerGetAnnouncementFiles, registerGetCourseContent, registerDownloadFile, registerGetClasslistEmails, registerGetRoster, registerGetMyGroups, registerGetSyllabus, registerGetDiscussions, registerGetVideoTranscript, registerGetServerInfo, registerSearchCourse, registerGetDropboxFolders, registerGetDropboxSubmissions, registerGetDropboxUserSubmissions, registerGetDropboxFeedback, registerGetRubricsForObject, registerDownloadDropboxSubmissionFile, } from "./tools/index.js";
|
|
20
20
|
import { registerWeeklyBriefingPrompt, registerGradeAuditPrompt, registerStudyPlannerPrompt, registerCourseSummaryPrompt, } from "./prompts/index.js";
|
|
21
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
22
|
const __dirname = dirname(__filename);
|
|
@@ -135,7 +135,14 @@ else {
|
|
|
135
135
|
registerGetVideoTranscript(server, apiClient);
|
|
136
136
|
registerGetServerInfo(server, config, PKG_VERSION, apiClient);
|
|
137
137
|
registerSearchCourse(server, apiClient);
|
|
138
|
-
|
|
138
|
+
// Instructor / TA dropbox tools (read-only) — 403 for a student account.
|
|
139
|
+
registerGetDropboxFolders(server, apiClient);
|
|
140
|
+
registerGetDropboxSubmissions(server, apiClient);
|
|
141
|
+
registerGetDropboxUserSubmissions(server, apiClient);
|
|
142
|
+
registerGetDropboxFeedback(server, apiClient);
|
|
143
|
+
registerGetRubricsForObject(server, apiClient);
|
|
144
|
+
registerDownloadDropboxSubmissionFile(server, apiClient);
|
|
145
|
+
log("DEBUG", "MCP tools registered (25 tools)");
|
|
139
146
|
// Register MCP prompts — surfaced in clients (e.g. Claude Desktop) as a
|
|
140
147
|
// picker, distinct from tools. Each one is a canned user message that
|
|
141
148
|
// names our own tools by name, so a client with no idea what to ask
|
|
@@ -148,7 +155,7 @@ else {
|
|
|
148
155
|
// Connect stdio transport
|
|
149
156
|
const transport = new StdioServerTransport();
|
|
150
157
|
await server.connect(transport);
|
|
151
|
-
log("INFO", "Brightspace MCP Server by Rohan Muppa — running on stdio (
|
|
158
|
+
log("INFO", "Brightspace MCP Server by Rohan Muppa — running on stdio (25 tools, 4 prompts registered)");
|
|
152
159
|
log("INFO", "Setup: see README.md for MCP client configuration (Claude Desktop, ChatGPT Desktop, Cursor, etc.)");
|
|
153
160
|
}
|
|
154
161
|
catch (error) {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError } from "../api/index.js";
|
|
7
|
+
import { DownloadDropboxSubmissionFileSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
import { validateContentId, MAX_FILE_SIZE } from "../utils/file-validator.js";
|
|
11
|
+
import { secureDownload } from "../utils/download-helpers.js";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
/**
|
|
15
|
+
* Register download_dropbox_submission_file tool.
|
|
16
|
+
*/
|
|
17
|
+
export function registerDownloadDropboxSubmissionFile(server, apiClient) {
|
|
18
|
+
server.registerTool("download_dropbox_submission_file", {
|
|
19
|
+
title: "Download Dropbox Submission File",
|
|
20
|
+
description: "Download a specific file from a student's (or group's) dropbox submission to a local directory, " +
|
|
21
|
+
"for an instructor or TA. Use get_dropbox_submissions or get_dropbox_user_submissions first to " +
|
|
22
|
+
"find submissionId and fileId. Requires instructor or TA access to the course. " +
|
|
23
|
+
"IMPORTANT: Ask the user where they want to save the file before calling this tool. " +
|
|
24
|
+
"After identifying the file, suggest a clean readable filename and ask if they'd like to rename it.",
|
|
25
|
+
inputSchema: DownloadDropboxSubmissionFileSchema,
|
|
26
|
+
}, async (args) => {
|
|
27
|
+
try {
|
|
28
|
+
log("DEBUG", "download_dropbox_submission_file tool called", { args });
|
|
29
|
+
const { courseId, folderId, submissionId, fileId, downloadPath, customFilename } = DownloadDropboxSubmissionFileSchema.parse(args);
|
|
30
|
+
validateContentId(courseId);
|
|
31
|
+
validateContentId(folderId);
|
|
32
|
+
validateContentId(submissionId);
|
|
33
|
+
validateContentId(fileId);
|
|
34
|
+
if (!path.isAbsolute(downloadPath)) {
|
|
35
|
+
return errorResponse("downloadPath must be an absolute path (e.g., /Users/username/Downloads or C:\\Users\\username\\Downloads)");
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const stats = await fs.stat(downloadPath);
|
|
39
|
+
if (!stats.isDirectory()) {
|
|
40
|
+
return errorResponse(`downloadPath is not a directory: ${downloadPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error?.code === "ENOENT") {
|
|
45
|
+
return errorResponse(`Download directory does not exist: ${downloadPath}`);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
// Fetch submission metadata to get the file list and validate fileId.
|
|
50
|
+
const submissionsPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/submissions/`);
|
|
51
|
+
let targetFile = null;
|
|
52
|
+
try {
|
|
53
|
+
const raw = await apiClient.get(submissionsPath);
|
|
54
|
+
const allSubmissions = Array.isArray(raw) ? raw : raw.Objects ?? [];
|
|
55
|
+
const submission = allSubmissions.find((s) => s.Id === submissionId);
|
|
56
|
+
if (!submission) {
|
|
57
|
+
return errorResponse(`Submission ${submissionId} not found in folder ${folderId}. ` +
|
|
58
|
+
"Use get_dropbox_submissions to list available submissions.");
|
|
59
|
+
}
|
|
60
|
+
targetFile = submission.Files.find((f) => f.FileId === fileId) ?? null;
|
|
61
|
+
if (!targetFile) {
|
|
62
|
+
const available = submission.Files.map((f) => `${f.FileName} (ID: ${f.FileId})`).join(", ");
|
|
63
|
+
return errorResponse(`File ID ${fileId} not found in submission ${submissionId}. Available files: ${available}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
68
|
+
return errorResponse("Instructor or TA access required for this course. This tool downloads another user's " +
|
|
69
|
+
"dropbox submission file, which Brightspace only grants to course staff.");
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
72
|
+
return errorResponse(`Folder ${folderId} not found in course ${courseId}.`);
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
// Check the size Brightspace reported before downloading anything.
|
|
77
|
+
if (targetFile.Size > MAX_FILE_SIZE) {
|
|
78
|
+
return errorResponse(`File too large (${Math.round(targetFile.Size / 1024 / 1024)}MB). ` +
|
|
79
|
+
`Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
80
|
+
}
|
|
81
|
+
const downloadApiPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/submissions/${submissionId}/files/${fileId}/download`);
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await apiClient.getRaw(downloadApiPath);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
88
|
+
return errorResponse("Instructor or TA access required for this course. Brightspace refused this download.");
|
|
89
|
+
}
|
|
90
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
91
|
+
return errorResponse("File not found on the server. It may have been deleted.");
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
// Check Content-Length before downloading the body (prevent memory exhaustion).
|
|
96
|
+
const contentLength = parseInt(response.headers.get("Content-Length") ?? "0", 10);
|
|
97
|
+
if (contentLength > MAX_FILE_SIZE) {
|
|
98
|
+
return errorResponse(`File too large (${Math.round(contentLength / 1024 / 1024)}MB). ` +
|
|
99
|
+
`Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
100
|
+
}
|
|
101
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
102
|
+
// Double-check the actual size once downloaded.
|
|
103
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
104
|
+
return errorResponse(`File too large (${Math.round(buffer.length / 1024 / 1024)}MB). ` +
|
|
105
|
+
`Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
106
|
+
}
|
|
107
|
+
const originalFilename = targetFile.FileName;
|
|
108
|
+
const effectiveFilename = customFilename || originalFilename;
|
|
109
|
+
// secureDownload applies this project's path-containment and
|
|
110
|
+
// magic-byte file-type checks and resolves filename conflicts,
|
|
111
|
+
// rather than writing the fetched buffer straight to disk.
|
|
112
|
+
const result = await secureDownload({
|
|
113
|
+
targetDir: downloadPath,
|
|
114
|
+
filename: effectiveFilename,
|
|
115
|
+
data: buffer,
|
|
116
|
+
});
|
|
117
|
+
log("INFO", `download_dropbox_submission_file: saved ${result.path} (${result.size} bytes, ${result.mime})`);
|
|
118
|
+
return toolResponse({
|
|
119
|
+
success: true,
|
|
120
|
+
filePath: result.path,
|
|
121
|
+
fileSize: result.size,
|
|
122
|
+
mimeType: result.mime,
|
|
123
|
+
originalFilename,
|
|
124
|
+
message: `File downloaded successfully to ${result.path}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return sanitizeError(error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError } from "../api/index.js";
|
|
7
|
+
import { GetDropboxFeedbackSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { convertHtmlToMarkdown } from "../utils/html-converter.js";
|
|
10
|
+
import { log } from "../utils/logger.js";
|
|
11
|
+
/**
|
|
12
|
+
* Register get_dropbox_feedback tool.
|
|
13
|
+
*/
|
|
14
|
+
export function registerGetDropboxFeedback(server, apiClient) {
|
|
15
|
+
server.registerTool("get_dropbox_feedback", {
|
|
16
|
+
title: "Get Dropbox Feedback",
|
|
17
|
+
description: "Retrieve the existing feedback already saved for a specific user or group in a dropbox folder, " +
|
|
18
|
+
"for an instructor or TA — score, graded state, feedback text, and rubric assessment detail. " +
|
|
19
|
+
"Use this before drafting new feedback to see what has already been recorded. Requires " +
|
|
20
|
+
"instructor or TA access to the course.",
|
|
21
|
+
inputSchema: GetDropboxFeedbackSchema,
|
|
22
|
+
}, async (args) => {
|
|
23
|
+
try {
|
|
24
|
+
log("DEBUG", "get_dropbox_feedback tool called", { args });
|
|
25
|
+
const { courseId, folderId, entityType, entityId } = GetDropboxFeedbackSchema.parse(args);
|
|
26
|
+
const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/${entityType}/${entityId}`);
|
|
27
|
+
let feedback;
|
|
28
|
+
try {
|
|
29
|
+
feedback = await apiClient.get(feedbackPath);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
33
|
+
return errorResponse("Instructor or TA access required for this course. This tool reads another user's " +
|
|
34
|
+
"grading feedback, which Brightspace only grants to course staff.");
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
37
|
+
return toolResponse({
|
|
38
|
+
courseId,
|
|
39
|
+
folderId,
|
|
40
|
+
entityType,
|
|
41
|
+
entityId,
|
|
42
|
+
feedback: null,
|
|
43
|
+
note: "No feedback found. This submission has not been graded yet.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
const feedbackMarkdown = feedback.Feedback?.Html
|
|
49
|
+
? convertHtmlToMarkdown(feedback.Feedback.Html).markdown
|
|
50
|
+
: null;
|
|
51
|
+
const rubricAssessments = feedback.RubricAssessments?.map((ra) => ({
|
|
52
|
+
rubricId: ra.RubricId,
|
|
53
|
+
rubricName: ra.Name,
|
|
54
|
+
totalPoints: ra.TotalPoints,
|
|
55
|
+
// Adapted from get-rubrics-for-object.ts's defensive ?? [] — some
|
|
56
|
+
// tenants return a null Criteria array on an unscored assessment.
|
|
57
|
+
criteria: (ra.Criteria ?? []).map((c) => ({
|
|
58
|
+
criterionId: c.CriterionId,
|
|
59
|
+
selectedLevelId: c.LevelId,
|
|
60
|
+
points: c.Points,
|
|
61
|
+
comments: c.Comments?.Text ?? null,
|
|
62
|
+
})),
|
|
63
|
+
})) ?? [];
|
|
64
|
+
log("INFO", `get_dropbox_feedback: retrieved feedback for ${entityType} ${entityId} in folder ${folderId}`);
|
|
65
|
+
return toolResponse({
|
|
66
|
+
courseId,
|
|
67
|
+
folderId,
|
|
68
|
+
entityType,
|
|
69
|
+
entityId,
|
|
70
|
+
feedback: {
|
|
71
|
+
score: feedback.Score,
|
|
72
|
+
isGraded: feedback.IsGraded,
|
|
73
|
+
feedbackText: feedback.Feedback?.Text ?? null,
|
|
74
|
+
feedbackMarkdown,
|
|
75
|
+
rubricAssessments,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return sanitizeError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError, DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
+
import { GetDropboxFoldersSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
/** Map SubmissionType integer to a readable label. */
|
|
11
|
+
function submissionTypeLabel(t) {
|
|
12
|
+
switch (t) {
|
|
13
|
+
case 0: return "file";
|
|
14
|
+
case 1: return "text";
|
|
15
|
+
case 2: return "on_paper";
|
|
16
|
+
case 3: return "observed_in_person";
|
|
17
|
+
default: return "unknown";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Register get_dropbox_folders tool.
|
|
22
|
+
*/
|
|
23
|
+
export function registerGetDropboxFolders(server, apiClient) {
|
|
24
|
+
server.registerTool("get_dropbox_folders", {
|
|
25
|
+
title: "Get Dropbox Folders",
|
|
26
|
+
description: "List all assignment/dropbox folders for a course, for an instructor or TA. Returns folder ID, " +
|
|
27
|
+
"name, due date, start/end dates, submission type, visibility, and whether rubrics are attached. " +
|
|
28
|
+
"Use this to discover folderId values before calling get_dropbox_submissions, " +
|
|
29
|
+
"get_dropbox_user_submissions, get_dropbox_feedback, or get_rubrics_for_object. " +
|
|
30
|
+
"This reads the same folder listing a student's own client uses, so it is not gated to " +
|
|
31
|
+
"instructor/TA accounts the way get_dropbox_submissions, get_dropbox_feedback, and " +
|
|
32
|
+
"download_dropbox_submission_file are.",
|
|
33
|
+
inputSchema: GetDropboxFoldersSchema,
|
|
34
|
+
}, async (args) => {
|
|
35
|
+
try {
|
|
36
|
+
log("DEBUG", "get_dropbox_folders tool called", { args });
|
|
37
|
+
const { courseId } = GetDropboxFoldersSchema.parse(args);
|
|
38
|
+
const apiPath = apiClient.le(courseId, "/dropbox/folders/");
|
|
39
|
+
let rawFolders;
|
|
40
|
+
try {
|
|
41
|
+
const response = await apiClient.get(apiPath, { ttl: DEFAULT_CACHE_TTLS.assignments });
|
|
42
|
+
rawFolders = Array.isArray(response) ? response : response.Objects ?? [];
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
46
|
+
return errorResponse("Instructor or TA access required for this course. This tool reads every student's " +
|
|
47
|
+
"dropbox folder metadata, which Brightspace only grants to course staff.");
|
|
48
|
+
}
|
|
49
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
50
|
+
return toolResponse({
|
|
51
|
+
courseId,
|
|
52
|
+
folders: [],
|
|
53
|
+
note: "No dropbox folders found for this course.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const folders = rawFolders.map((f) => ({
|
|
59
|
+
folderId: f.Id,
|
|
60
|
+
name: f.Name,
|
|
61
|
+
categoryId: f.CategoryId,
|
|
62
|
+
isHidden: f.IsHidden,
|
|
63
|
+
isGroup: f.GroupTypeId !== null,
|
|
64
|
+
submissionType: submissionTypeLabel(f.SubmissionType),
|
|
65
|
+
dueDate: f.DueDate ?? null,
|
|
66
|
+
startDate: f.StartDate ?? null,
|
|
67
|
+
endDate: f.EndDate ?? null,
|
|
68
|
+
maxScore: f.Assessment?.ScoreDenominator ?? null,
|
|
69
|
+
rubrics: f.Assessment?.Rubrics?.map((r) => ({
|
|
70
|
+
rubricId: r.RubricId,
|
|
71
|
+
name: r.Name,
|
|
72
|
+
})) ?? [],
|
|
73
|
+
hasRubrics: (f.Assessment?.Rubrics?.length ?? 0) > 0,
|
|
74
|
+
}));
|
|
75
|
+
log("INFO", `get_dropbox_folders: ${folders.length} folders for course ${courseId}`);
|
|
76
|
+
return toolResponse({
|
|
77
|
+
courseId,
|
|
78
|
+
folders,
|
|
79
|
+
...(folders.length === 0 ? { note: "No dropbox folders found for this course." } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return sanitizeError(error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError, DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
+
import { GetDropboxSubmissionsSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
// Per-submitter feedback lookup is one GET each; cap how many run at once so
|
|
11
|
+
// a large dropbox folder can't fan out hundreds of concurrent requests.
|
|
12
|
+
const FEEDBACK_FETCH_CONCURRENCY = 8;
|
|
13
|
+
/** Run `fn` over `items`, at most `concurrency` in flight at a time. */
|
|
14
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
15
|
+
let next = 0;
|
|
16
|
+
async function worker() {
|
|
17
|
+
while (next < items.length) {
|
|
18
|
+
const item = items[next++];
|
|
19
|
+
await fn(item);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register get_dropbox_submissions tool.
|
|
26
|
+
*/
|
|
27
|
+
export function registerGetDropboxSubmissions(server, apiClient) {
|
|
28
|
+
server.registerTool("get_dropbox_submissions", {
|
|
29
|
+
title: "Get Dropbox Submissions",
|
|
30
|
+
description: "List every student's (or group's) submission to a dropbox folder, for an instructor or TA — " +
|
|
31
|
+
"submitter names, submission dates, late status, file lists, and feedback/grading status. " +
|
|
32
|
+
"Use get_dropbox_folders first to find folderId. Requires instructor or TA access; a student " +
|
|
33
|
+
"account gets a clear note instead of data (a student's own download_file / get_assignments " +
|
|
34
|
+
"already cover their own submission). Results are capped by limit (default 100, max 1000); " +
|
|
35
|
+
"a truncated response says so and only fetches feedback for the returned slice.",
|
|
36
|
+
inputSchema: GetDropboxSubmissionsSchema,
|
|
37
|
+
}, async (args) => {
|
|
38
|
+
try {
|
|
39
|
+
log("DEBUG", "get_dropbox_submissions tool called", { args });
|
|
40
|
+
const { courseId, folderId, activeOnly, ignoreFeedback, limit } = GetDropboxSubmissionsSchema.parse(args);
|
|
41
|
+
const folderPath = apiClient.le(courseId, "/dropbox/folders/");
|
|
42
|
+
const submissionsPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/submissions/`);
|
|
43
|
+
let rawSubmissions;
|
|
44
|
+
let folder = null;
|
|
45
|
+
try {
|
|
46
|
+
const [foldersRaw, submissionsRaw] = await Promise.all([
|
|
47
|
+
apiClient.get(folderPath, { ttl: DEFAULT_CACHE_TTLS.assignments }),
|
|
48
|
+
apiClient.get(submissionsPath),
|
|
49
|
+
]);
|
|
50
|
+
const allFolders = Array.isArray(foldersRaw) ? foldersRaw : foldersRaw.Objects ?? [];
|
|
51
|
+
folder = allFolders.find((f) => f.Id === folderId) ?? null;
|
|
52
|
+
rawSubmissions = Array.isArray(submissionsRaw) ? submissionsRaw : submissionsRaw.Objects ?? [];
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
56
|
+
return errorResponse("Instructor or TA access required for this course. This tool reads every student's " +
|
|
57
|
+
"dropbox submissions, which Brightspace only grants to course staff.");
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
60
|
+
return toolResponse({
|
|
61
|
+
courseId,
|
|
62
|
+
folderId,
|
|
63
|
+
folderName: null,
|
|
64
|
+
dueDate: null,
|
|
65
|
+
submissions: [],
|
|
66
|
+
note: `Dropbox folder ${folderId} not found in course ${courseId}.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (rawSubmissions.length === 0) {
|
|
72
|
+
return toolResponse({
|
|
73
|
+
courseId,
|
|
74
|
+
folderId,
|
|
75
|
+
folderName: folder?.Name ?? null,
|
|
76
|
+
dueDate: folder?.DueDate ?? null,
|
|
77
|
+
submissions: [],
|
|
78
|
+
note: "No submissions found for this dropbox folder.",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Cap the fan-out up front: a large dropbox folder could otherwise
|
|
82
|
+
// mean hundreds of concurrent per-submitter feedback GETs and an
|
|
83
|
+
// unbounded payload. Only the kept slice gets a feedback lookup.
|
|
84
|
+
const total = rawSubmissions.length;
|
|
85
|
+
const truncated = total > limit;
|
|
86
|
+
const kept = truncated ? rawSubmissions.slice(0, limit) : rawSubmissions;
|
|
87
|
+
// Optionally fetch feedback for each kept submitter, a bounded
|
|
88
|
+
// number at a time. Best-effort: a submitter with no feedback yet
|
|
89
|
+
// simply has none in the map.
|
|
90
|
+
const feedbackMap = new Map();
|
|
91
|
+
if (!ignoreFeedback) {
|
|
92
|
+
await mapWithConcurrency(kept, FEEDBACK_FETCH_CONCURRENCY, async (sub) => {
|
|
93
|
+
try {
|
|
94
|
+
const userId = sub.SubmittedBy.Identifier;
|
|
95
|
+
const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/user/${userId}`);
|
|
96
|
+
const fb = await apiClient.get(feedbackPath);
|
|
97
|
+
feedbackMap.set(userId, fb);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// No feedback yet, or this submitter's feedback route
|
|
101
|
+
// otherwise failed — silently skip, matching the "not
|
|
102
|
+
// graded yet" default below.
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const dueDate = folder?.DueDate ? new Date(folder.DueDate) : null;
|
|
107
|
+
const maxScore = folder?.Assessment?.ScoreDenominator ?? null;
|
|
108
|
+
const submissions = kept.map((sub) => {
|
|
109
|
+
const userId = sub.SubmittedBy.Identifier;
|
|
110
|
+
const submittedAt = new Date(sub.SubmissionDate);
|
|
111
|
+
const isLate = dueDate !== null && submittedAt > dueDate;
|
|
112
|
+
const feedback = feedbackMap.get(userId) ?? null;
|
|
113
|
+
const entry = {
|
|
114
|
+
submissionId: sub.Id,
|
|
115
|
+
userId,
|
|
116
|
+
displayName: sub.SubmittedBy.DisplayName,
|
|
117
|
+
submittedDate: sub.SubmissionDate,
|
|
118
|
+
isLate,
|
|
119
|
+
files: sub.Files.map((f) => ({
|
|
120
|
+
fileId: f.FileId,
|
|
121
|
+
name: f.FileName,
|
|
122
|
+
size: f.Size,
|
|
123
|
+
})),
|
|
124
|
+
comment: sub.Comment?.Text ?? null,
|
|
125
|
+
};
|
|
126
|
+
if (!ignoreFeedback) {
|
|
127
|
+
entry.feedbackStatus = feedback
|
|
128
|
+
? {
|
|
129
|
+
isGraded: feedback.IsGraded,
|
|
130
|
+
score: feedback.Score,
|
|
131
|
+
maxScore,
|
|
132
|
+
hasFeedbackText: !!feedback.Feedback?.Text,
|
|
133
|
+
}
|
|
134
|
+
: { isGraded: false, score: null, maxScore, hasFeedbackText: false };
|
|
135
|
+
}
|
|
136
|
+
return entry;
|
|
137
|
+
});
|
|
138
|
+
// activeOnly: skip submissions that are already fully graded.
|
|
139
|
+
const filtered = activeOnly && !ignoreFeedback
|
|
140
|
+
? submissions.filter((s) => !s.feedbackStatus?.isGraded)
|
|
141
|
+
: submissions;
|
|
142
|
+
log("INFO", `get_dropbox_submissions: ${filtered.length} submissions (of ${total} total) for folder ${folderId}`, { truncated });
|
|
143
|
+
return toolResponse({
|
|
144
|
+
courseId,
|
|
145
|
+
folderId,
|
|
146
|
+
folderName: folder?.Name ?? null,
|
|
147
|
+
dueDate: folder?.DueDate ?? null,
|
|
148
|
+
maxScore,
|
|
149
|
+
totalSubmissions: total,
|
|
150
|
+
returnedSubmissions: filtered.length,
|
|
151
|
+
truncated,
|
|
152
|
+
...(truncated
|
|
153
|
+
? { note: `Showing ${kept.length} of ${total} submissions. Raise the limit argument to see more.` }
|
|
154
|
+
: {}),
|
|
155
|
+
activeOnly,
|
|
156
|
+
submissions: filtered,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return sanitizeError(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError } from "../api/index.js";
|
|
7
|
+
import { GetDropboxUserSubmissionsSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register get_dropbox_user_submissions tool.
|
|
12
|
+
*/
|
|
13
|
+
export function registerGetDropboxUserSubmissions(server, apiClient) {
|
|
14
|
+
server.registerTool("get_dropbox_user_submissions", {
|
|
15
|
+
title: "Get Dropbox User Submissions",
|
|
16
|
+
description: "Retrieve all submissions made by one specific student (or group) in a dropbox folder, for an " +
|
|
17
|
+
"instructor or TA — useful for reviewing a single student's work before drafting feedback. " +
|
|
18
|
+
"Use get_dropbox_submissions or get_roster to find the userId first. Requires instructor or TA " +
|
|
19
|
+
"access to the course.",
|
|
20
|
+
inputSchema: GetDropboxUserSubmissionsSchema,
|
|
21
|
+
}, async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
log("DEBUG", "get_dropbox_user_submissions tool called", { args });
|
|
24
|
+
const { courseId, folderId, userId, ignoreFeedback } = GetDropboxUserSubmissionsSchema.parse(args);
|
|
25
|
+
const submissionsPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/submissions/users/${userId}`);
|
|
26
|
+
let rawSubmissions;
|
|
27
|
+
try {
|
|
28
|
+
const raw = await apiClient.get(submissionsPath);
|
|
29
|
+
rawSubmissions = Array.isArray(raw) ? raw : raw.Objects ?? [];
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
33
|
+
return errorResponse("Instructor or TA access required for this course. This tool reads another user's " +
|
|
34
|
+
"dropbox submissions, which Brightspace only grants to course staff.");
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
37
|
+
return toolResponse({
|
|
38
|
+
courseId,
|
|
39
|
+
folderId,
|
|
40
|
+
userId,
|
|
41
|
+
submissions: [],
|
|
42
|
+
note: `No submissions found for user ${userId} in dropbox folder ${folderId}. ` +
|
|
43
|
+
"The user may not have submitted, or the folder may not exist.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
if (rawSubmissions.length === 0) {
|
|
49
|
+
return toolResponse({
|
|
50
|
+
courseId,
|
|
51
|
+
folderId,
|
|
52
|
+
userId,
|
|
53
|
+
submissions: [],
|
|
54
|
+
note: "No submissions found for this user in the specified folder.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
let feedback = null;
|
|
58
|
+
if (!ignoreFeedback) {
|
|
59
|
+
try {
|
|
60
|
+
const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/user/${userId}`);
|
|
61
|
+
feedback = await apiClient.get(feedbackPath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// No feedback yet — silently skip, matching the "not graded yet" default below.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const submissions = rawSubmissions.map((sub) => ({
|
|
68
|
+
submissionId: sub.Id,
|
|
69
|
+
userId: sub.SubmittedBy.Identifier,
|
|
70
|
+
displayName: sub.SubmittedBy.DisplayName,
|
|
71
|
+
submittedDate: sub.SubmissionDate,
|
|
72
|
+
files: sub.Files.map((f) => ({
|
|
73
|
+
fileId: f.FileId,
|
|
74
|
+
name: f.FileName,
|
|
75
|
+
size: f.Size,
|
|
76
|
+
})),
|
|
77
|
+
comment: sub.Comment?.Text ?? null,
|
|
78
|
+
}));
|
|
79
|
+
const feedbackStatus = ignoreFeedback
|
|
80
|
+
? undefined
|
|
81
|
+
: feedback
|
|
82
|
+
? {
|
|
83
|
+
isGraded: feedback.IsGraded,
|
|
84
|
+
score: feedback.Score,
|
|
85
|
+
hasFeedbackText: !!feedback.Feedback?.Text,
|
|
86
|
+
feedbackText: feedback.Feedback?.Text ?? null,
|
|
87
|
+
}
|
|
88
|
+
: { isGraded: false, score: null, hasFeedbackText: false, feedbackText: null };
|
|
89
|
+
log("INFO", `get_dropbox_user_submissions: ${submissions.length} submissions for user ${userId} in folder ${folderId}`);
|
|
90
|
+
return toolResponse({
|
|
91
|
+
courseId,
|
|
92
|
+
folderId,
|
|
93
|
+
userId,
|
|
94
|
+
submissions,
|
|
95
|
+
feedbackStatus,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return sanitizeError(error);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purdue Brightspace MCP Server
|
|
3
|
+
* Copyright (c) 2026 Rohan Muppa. All rights reserved.
|
|
4
|
+
* Licensed under MIT — see LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { ApiError, DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
|
+
import { GetRubricsForObjectSchema } from "./schemas.js";
|
|
8
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register get_rubrics_for_object tool.
|
|
12
|
+
*/
|
|
13
|
+
export function registerGetRubricsForObject(server, apiClient) {
|
|
14
|
+
server.registerTool("get_rubrics_for_object", {
|
|
15
|
+
title: "Get Rubrics for Dropbox Folder",
|
|
16
|
+
description: "Retrieve the full rubric table (criteria, levels, and point values) attached to a dropbox " +
|
|
17
|
+
"folder, for an instructor or TA. Use get_dropbox_folders first to find folderId. Like " +
|
|
18
|
+
"get_dropbox_folders, this reads the folder listing's embedded rubric data, so it is not " +
|
|
19
|
+
"gated to instructor/TA accounts the way get_dropbox_submissions and get_dropbox_feedback are.",
|
|
20
|
+
inputSchema: GetRubricsForObjectSchema,
|
|
21
|
+
}, async (args) => {
|
|
22
|
+
try {
|
|
23
|
+
log("DEBUG", "get_rubrics_for_object tool called", { args });
|
|
24
|
+
const { courseId, folderId } = GetRubricsForObjectSchema.parse(args);
|
|
25
|
+
const apiPath = apiClient.le(courseId, "/dropbox/folders/");
|
|
26
|
+
let rawFolders;
|
|
27
|
+
try {
|
|
28
|
+
const response = await apiClient.get(apiPath, { ttl: DEFAULT_CACHE_TTLS.assignments });
|
|
29
|
+
rawFolders = Array.isArray(response) ? response : response.Objects ?? [];
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof ApiError && error.status === 403) {
|
|
33
|
+
return errorResponse("Instructor or TA access required for this course. This tool reads dropbox rubric " +
|
|
34
|
+
"metadata, which Brightspace only grants to course staff.");
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
37
|
+
return toolResponse({
|
|
38
|
+
courseId,
|
|
39
|
+
folderId,
|
|
40
|
+
folderName: null,
|
|
41
|
+
rubrics: [],
|
|
42
|
+
note: `Course ${courseId} was not found.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const folder = rawFolders.find((f) => f.Id === folderId);
|
|
48
|
+
if (!folder) {
|
|
49
|
+
return toolResponse({
|
|
50
|
+
courseId,
|
|
51
|
+
folderId,
|
|
52
|
+
folderName: null,
|
|
53
|
+
rubrics: [],
|
|
54
|
+
note: `Dropbox folder ${folderId} not found in course ${courseId}.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const rubrics = folder.Assessment?.Rubrics;
|
|
58
|
+
if (!rubrics || rubrics.length === 0) {
|
|
59
|
+
return toolResponse({
|
|
60
|
+
courseId,
|
|
61
|
+
folderId,
|
|
62
|
+
folderName: folder.Name,
|
|
63
|
+
rubrics: [],
|
|
64
|
+
note: "No rubrics are attached to this dropbox folder.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const formattedRubrics = rubrics.map((r) => ({
|
|
68
|
+
rubricId: r.RubricId,
|
|
69
|
+
name: r.Name,
|
|
70
|
+
criteria: (r.Criteria ?? []).map((c) => ({
|
|
71
|
+
criterionId: c.CriterionId,
|
|
72
|
+
name: c.Name,
|
|
73
|
+
levels: (c.Levels ?? []).map((l) => ({
|
|
74
|
+
levelId: l.LevelId,
|
|
75
|
+
name: l.Name,
|
|
76
|
+
points: l.Points,
|
|
77
|
+
description: l.Description?.Text ?? null,
|
|
78
|
+
})),
|
|
79
|
+
})),
|
|
80
|
+
}));
|
|
81
|
+
log("INFO", `get_rubrics_for_object: ${formattedRubrics.length} rubric(s) for folder ${folderId}`);
|
|
82
|
+
return toolResponse({
|
|
83
|
+
courseId,
|
|
84
|
+
folderId,
|
|
85
|
+
folderName: folder.Name,
|
|
86
|
+
maxScore: folder.Assessment?.ScoreDenominator ?? null,
|
|
87
|
+
rubrics: formattedRubrics,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return sanitizeError(error);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
package/build/tools/index.js
CHANGED
|
@@ -23,6 +23,13 @@ export { registerGetDiscussions } from "./get-discussions.js";
|
|
|
23
23
|
export { registerGetVideoTranscript } from "./get-video-transcript.js";
|
|
24
24
|
export { registerGetServerInfo } from "./get-server-info.js";
|
|
25
25
|
export { registerSearchCourse } from "./search-course.js";
|
|
26
|
+
// Instructor / TA dropbox tools (read-only)
|
|
27
|
+
export { registerGetDropboxFolders } from "./get-dropbox-folders.js";
|
|
28
|
+
export { registerGetDropboxSubmissions } from "./get-dropbox-submissions.js";
|
|
29
|
+
export { registerGetDropboxUserSubmissions } from "./get-dropbox-user-submissions.js";
|
|
30
|
+
export { registerGetDropboxFeedback } from "./get-dropbox-feedback.js";
|
|
31
|
+
export { registerGetRubricsForObject } from "./get-rubrics-for-object.js";
|
|
32
|
+
export { registerDownloadDropboxSubmissionFile } from "./download-dropbox-submission-file.js";
|
|
26
33
|
// Re-export shared helpers and schemas for convenience
|
|
27
34
|
export { toolResponse, errorResponse, sanitizeError } from "./tool-helpers.js";
|
|
28
35
|
export * from "./schemas.js";
|
package/build/tools/schemas.js
CHANGED
|
@@ -156,3 +156,64 @@ export const GetMyGroupsSchema = z.object({
|
|
|
156
156
|
.describe("Course ID to list the current user's project/discussion groups for."),
|
|
157
157
|
});
|
|
158
158
|
export const GetServerInfoSchema = z.object({});
|
|
159
|
+
// ── Instructor / TA dropbox tools ──────────────────────────────────────────
|
|
160
|
+
// Read-only, instructor/TA-side views of a course's dropbox (assignment)
|
|
161
|
+
// folders and the submissions inside them. These answer "who has/hasn't
|
|
162
|
+
// submitted" and "what feedback has already been given", distinct from the
|
|
163
|
+
// student-facing tools above which only ever see the caller's own work.
|
|
164
|
+
export const GetDropboxFoldersSchema = z.object({
|
|
165
|
+
courseId: z.coerce.number().int().positive()
|
|
166
|
+
.describe("Course ID to list dropbox (assignment) folders for."),
|
|
167
|
+
});
|
|
168
|
+
export const GetDropboxSubmissionsSchema = z.object({
|
|
169
|
+
courseId: z.coerce.number().int().positive()
|
|
170
|
+
.describe("Course ID the dropbox folder belongs to."),
|
|
171
|
+
folderId: z.coerce.number().int().positive()
|
|
172
|
+
.describe("Dropbox folder ID to list submissions for. Use get_dropbox_folders to find it."),
|
|
173
|
+
activeOnly: z.boolean().default(false)
|
|
174
|
+
.describe("When true, skip submissions that have already been fully graded (feedback published with a score). Default false: returns every submission, graded and ungraded."),
|
|
175
|
+
ignoreFeedback: z.boolean().default(false)
|
|
176
|
+
.describe("When true, omit feedback status from each submission entry to reduce response size."),
|
|
177
|
+
limit: z.coerce.number().int().positive().max(1000).default(100)
|
|
178
|
+
.describe("Maximum submissions to return. Default 100. The response reports the true total and whether it was truncated; feedback is only fetched for the returned slice."),
|
|
179
|
+
});
|
|
180
|
+
export const GetDropboxUserSubmissionsSchema = z.object({
|
|
181
|
+
courseId: z.coerce.number().int().positive()
|
|
182
|
+
.describe("Course ID the dropbox folder belongs to."),
|
|
183
|
+
folderId: z.coerce.number().int().positive()
|
|
184
|
+
.describe("Dropbox folder ID."),
|
|
185
|
+
userId: z.coerce.number().int().positive()
|
|
186
|
+
.describe("Brightspace user ID (or group ID for a group folder) whose submissions to retrieve."),
|
|
187
|
+
ignoreFeedback: z.boolean().default(false)
|
|
188
|
+
.describe("When true, omit feedback status from the response."),
|
|
189
|
+
});
|
|
190
|
+
export const GetDropboxFeedbackSchema = z.object({
|
|
191
|
+
courseId: z.coerce.number().int().positive()
|
|
192
|
+
.describe("Course ID the dropbox folder belongs to."),
|
|
193
|
+
folderId: z.coerce.number().int().positive()
|
|
194
|
+
.describe("Dropbox folder ID."),
|
|
195
|
+
entityType: z.enum(["user", "group"])
|
|
196
|
+
.describe("Whether entityId identifies an individual user or a group."),
|
|
197
|
+
entityId: z.coerce.number().int().positive()
|
|
198
|
+
.describe("User ID or group ID whose existing feedback to retrieve."),
|
|
199
|
+
});
|
|
200
|
+
export const GetRubricsForObjectSchema = z.object({
|
|
201
|
+
courseId: z.coerce.number().int().positive()
|
|
202
|
+
.describe("Course ID the dropbox folder belongs to."),
|
|
203
|
+
folderId: z.coerce.number().int().positive()
|
|
204
|
+
.describe("Dropbox folder ID whose rubric(s) to retrieve full criteria/level detail for."),
|
|
205
|
+
});
|
|
206
|
+
export const DownloadDropboxSubmissionFileSchema = z.object({
|
|
207
|
+
courseId: z.coerce.number().int().positive()
|
|
208
|
+
.describe("Course ID the dropbox folder belongs to."),
|
|
209
|
+
folderId: z.coerce.number().int().positive()
|
|
210
|
+
.describe("Dropbox folder ID."),
|
|
211
|
+
submissionId: z.coerce.number().int().positive()
|
|
212
|
+
.describe("Submission ID, from get_dropbox_submissions or get_dropbox_user_submissions."),
|
|
213
|
+
fileId: z.coerce.number().int().positive()
|
|
214
|
+
.describe("File ID within that submission."),
|
|
215
|
+
downloadPath: z.string().min(1)
|
|
216
|
+
.describe("Absolute path to the local directory where the file should be saved."),
|
|
217
|
+
customFilename: z.string().max(255).optional()
|
|
218
|
+
.describe("Custom filename for the downloaded file (include extension). If omitted, uses the original submission filename."),
|
|
219
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.17",
|
|
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",
|