brightspace-mcp-server 5.2.4 → 5.2.6
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.
|
@@ -414,6 +414,19 @@ function feedbackPageListsSubmission(html) {
|
|
|
414
414
|
}
|
|
415
415
|
return false;
|
|
416
416
|
}
|
|
417
|
+
function assignmentListCoverage(reason) {
|
|
418
|
+
const status = typeof reason?.status === "number" ? reason.status : null;
|
|
419
|
+
if (status === 404)
|
|
420
|
+
return {};
|
|
421
|
+
return {
|
|
422
|
+
assignmentListUnavailable: { status },
|
|
423
|
+
assignmentListNote: (status === 403
|
|
424
|
+
? "Brightspace refused this course's assignment (dropbox) list (403), "
|
|
425
|
+
: `Brightspace did not return this course's assignment (dropbox) list${status !== null ? ` (it answered ${status})` : ""}, `) +
|
|
426
|
+
"so its assignments are missing from this answer. Do not tell the user this course has no " +
|
|
427
|
+
"assignments due; call get_assignments again for it, or tell them to check Assignments in Brightspace.",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
417
430
|
/** A course's assignments and quizzes, with whether its quiz list is complete. */
|
|
418
431
|
async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
419
432
|
const assignments = [];
|
|
@@ -633,8 +646,9 @@ async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
|
633
646
|
}
|
|
634
647
|
}
|
|
635
648
|
else {
|
|
636
|
-
//
|
|
637
|
-
|
|
649
|
+
// Don't throw: the quizzes and gradebook may still have answered. The
|
|
650
|
+
// failure is reported through assignmentListCoverage below.
|
|
651
|
+
log("INFO", `Failed to fetch dropbox folders for course ${courseId}`, dropboxResult.reason);
|
|
638
652
|
}
|
|
639
653
|
// Process quizzes. Content discovery remains useful when the list route is
|
|
640
654
|
// forbidden or unavailable, so a failed list starts from an empty set.
|
|
@@ -809,6 +823,7 @@ async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
|
809
823
|
gradebook: gradebookResult.status === "fulfilled",
|
|
810
824
|
contentRecovery,
|
|
811
825
|
}),
|
|
826
|
+
...(dropboxResult.status === "rejected" ? assignmentListCoverage(dropboxResult.reason) : {}),
|
|
812
827
|
};
|
|
813
828
|
}
|
|
814
829
|
/**
|
|
@@ -877,7 +892,9 @@ export function registerGetAssignments(server, apiClient, config) {
|
|
|
877
892
|
"availability (notYetOpen, open, closed) is worked out from the quiz's own start and end dates and is left out when it has none. exempt: true means Brightspace marks the user exempt from that quiz's topic; the quiz is still listed. " +
|
|
878
893
|
"Each course also carries quizCoverage. \"complete\" means every quiz source answered, not that every quiz is guaranteed to be listed; \"incomplete\" means a quiz source could not be read, so the course may have quizzes not listed. " +
|
|
879
894
|
"Never conclude that a quiz was missed or not taken because it is absent, and never present the listed quizzes as all of them. " +
|
|
880
|
-
"A gradeOnly row named like a quiz is a quiz whose details could not be read."
|
|
895
|
+
"A gradeOnly row named like a quiz is a quiz whose details could not be read. " +
|
|
896
|
+
"A course with assignmentListUnavailable had its assignment list unread: its assignments are missing, not absent, so never say it has none due. " +
|
|
897
|
+
"Without courseId, courses listed in unreadCourses could not be read at all and are not in courses; never report them as having nothing due.",
|
|
881
898
|
inputSchema: GetAssignmentsSchema,
|
|
882
899
|
}, async (args) => {
|
|
883
900
|
try {
|
|
@@ -944,11 +961,6 @@ export function registerGetAssignments(server, apiClient, config) {
|
|
|
944
961
|
};
|
|
945
962
|
}
|
|
946
963
|
catch (error) {
|
|
947
|
-
// 403 means no access (past course, etc) - log and skip
|
|
948
|
-
if (error?.status === 403) {
|
|
949
|
-
log("DEBUG", `get_assignments: 403 Forbidden for course ${item.OrgUnit.Id} (${item.OrgUnit.Name}) - skipping`);
|
|
950
|
-
return null;
|
|
951
|
-
}
|
|
952
964
|
// A pending sign-in only means this course's routes never
|
|
953
965
|
// answered — it says nothing about the other courses, whose
|
|
954
966
|
// requests may already have gone out independently. Mark this one
|
|
@@ -963,13 +975,26 @@ export function registerGetAssignments(server, apiClient, config) {
|
|
|
963
975
|
authError: error,
|
|
964
976
|
};
|
|
965
977
|
}
|
|
966
|
-
throw error;
|
|
978
|
+
throw error;
|
|
967
979
|
}
|
|
968
980
|
});
|
|
969
981
|
const results = await Promise.allSettled(assignmentPromises);
|
|
970
982
|
const settled = results
|
|
971
|
-
.filter((r) => r.status === "fulfilled"
|
|
983
|
+
.filter((r) => r.status === "fulfilled")
|
|
972
984
|
.map((r) => r.value);
|
|
985
|
+
// A course whose inventory failed (a 403 on a past course, a 5xx, a
|
|
986
|
+
// malformed payload) is not a course with nothing due. It stays out
|
|
987
|
+
// of `courses`, as before, and is named here instead.
|
|
988
|
+
// **Why:** these courses used to vanish with no trace, so "what's
|
|
989
|
+
// due?" silently skipped a class (#447).
|
|
990
|
+
const unreadCourses = results.flatMap((result, index) => {
|
|
991
|
+
if (result.status !== "rejected")
|
|
992
|
+
return [];
|
|
993
|
+
const item = filteredEnrollments[index];
|
|
994
|
+
const status = typeof result.reason?.status === "number" ? result.reason.status : null;
|
|
995
|
+
log("INFO", `get_assignments: could not read course ${item.OrgUnit.Id} (${item.OrgUnit.Name})`, result.reason);
|
|
996
|
+
return [{ courseId: item.OrgUnit.Id, courseName: item.OrgUnit.Name, status }];
|
|
997
|
+
});
|
|
973
998
|
const pending = settled.filter((c) => c.authPending);
|
|
974
999
|
const courses = settled
|
|
975
1000
|
.filter((c) => !c.authPending)
|
|
@@ -990,16 +1015,25 @@ export function registerGetAssignments(server, apiClient, config) {
|
|
|
990
1015
|
})),
|
|
991
1016
|
],
|
|
992
1017
|
};
|
|
1018
|
+
const notices = [];
|
|
993
1019
|
if (pending.length > 0) {
|
|
994
1020
|
throwIfCredentialRejected(pending[0].authError);
|
|
995
1021
|
response.authPending = true;
|
|
996
1022
|
response.unavailableCourseIds = pending.map((c) => c.courseId);
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1023
|
+
notices.push("Sign-in to Brightspace is still in progress, so assignments for " +
|
|
1024
|
+
`${pending.length} course(s) (${pending.map((c) => c.courseId).join(", ")}) could not be ` +
|
|
1025
|
+
`fetched yet. ${authPendingNotice(pending[0].authError)} Call get_assignments again once ` +
|
|
1026
|
+
"sign-in finishes.");
|
|
1027
|
+
}
|
|
1028
|
+
if (unreadCourses.length > 0) {
|
|
1029
|
+
response.unreadCourses = unreadCourses;
|
|
1030
|
+
notices.push(`Assignments for ${unreadCourses.length} course(s) could not be read from Brightspace ` +
|
|
1031
|
+
`(${unreadCourses.map((c) => `"${c.courseName}" ${c.courseId}`).join(", ")}), so they are ` +
|
|
1032
|
+
"missing from this list. Do not tell the user those courses have nothing due: call " +
|
|
1033
|
+
"get_assignments again with that courseId, or tell them to check Brightspace.");
|
|
1002
1034
|
}
|
|
1035
|
+
if (notices.length > 0)
|
|
1036
|
+
response.notice = notices.join(" ");
|
|
1003
1037
|
return toolResponse(response);
|
|
1004
1038
|
}
|
|
1005
1039
|
catch (error) {
|
|
@@ -14,6 +14,8 @@ import { log } from "../utils/logger.js";
|
|
|
14
14
|
const SELF_ENROLLMENT_STYLES = new Set([3, 4, 5]);
|
|
15
15
|
/** Without a courseId, courses that started longer ago than this are skipped unless includeOlder is set. */
|
|
16
16
|
const RECENT_COURSE_MS = 365 * 24 * 60 * 60 * 1000;
|
|
17
|
+
/** Why a category's groups couldn't be read, in words an agent can relay. */
|
|
18
|
+
const unreadReason = (status) => status === 403 ? "Brightspace refused access (403)" : "Brightspace found no group list (404)";
|
|
17
19
|
const inaccessible = (error) => error instanceof ApiError && (error.status === 403 || error.status === 404);
|
|
18
20
|
const text = (value) => value?.Text?.trim() || undefined;
|
|
19
21
|
/**
|
|
@@ -27,7 +29,7 @@ function signUpOpen(category, now) {
|
|
|
27
29
|
const end = category.SelfEnrollmentExpiryDate ? Date.parse(category.SelfEnrollmentExpiryDate) : Infinity;
|
|
28
30
|
return start <= now && now <= end;
|
|
29
31
|
}
|
|
30
|
-
async function fetchCourseGroups(apiClient, course, myId, now) {
|
|
32
|
+
async function fetchCourseGroups(apiClient, course, myId, now, unreadOut) {
|
|
31
33
|
const result = { courseId: course.id, courseName: course.name, categories: [], alreadyInGroup: [] };
|
|
32
34
|
let categories;
|
|
33
35
|
try {
|
|
@@ -47,6 +49,7 @@ async function fetchCourseGroups(apiClient, course, myId, now) {
|
|
|
47
49
|
catch (error) {
|
|
48
50
|
if (inaccessible(error)) {
|
|
49
51
|
unread.push(category.Name);
|
|
52
|
+
unreadOut.push({ courseId: course.id, name: category.Name, status: error.status });
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
52
55
|
throw error;
|
|
@@ -115,7 +118,8 @@ export function registerGetAvailableGroups(server, apiClient, config) {
|
|
|
115
118
|
return Number.isFinite(start) && start >= now - RECENT_COURSE_MS;
|
|
116
119
|
});
|
|
117
120
|
const olderSkipped = enrolled.length - courses.length;
|
|
118
|
-
const
|
|
121
|
+
const unreadCategories = [];
|
|
122
|
+
const results = await Promise.allSettled(courses.map((course) => fetchCourseGroups(apiClient, course, myId, now, unreadCategories)));
|
|
119
123
|
const pendingCourseIds = [];
|
|
120
124
|
const failedCourseIds = [];
|
|
121
125
|
let firstAuthError = null;
|
|
@@ -149,6 +153,16 @@ export function registerGetAvailableGroups(server, apiClient, config) {
|
|
|
149
153
|
if (failedCourseIds.length > 0) {
|
|
150
154
|
notes.push(`Couldn't read groups for course(s) ${failedCourseIds.join(", ")}, so open sign-ups there aren't listed.`);
|
|
151
155
|
}
|
|
156
|
+
// Without a reason attached, a bare unreadCategories list reads like a
|
|
157
|
+
// category with nothing open, so name each one and say why it's there (#433).
|
|
158
|
+
const shownCourseIds = new Set(withChoices.map((c) => c.courseId));
|
|
159
|
+
const unread = unreadCategories.filter((u) => shownCourseIds.has(u.courseId));
|
|
160
|
+
if (unread.length > 0) {
|
|
161
|
+
const where = unread.map((u) => `"${u.name}" in course ${u.courseId}: ${unreadReason(u.status)}`).join("; ");
|
|
162
|
+
notes.push(`Couldn't read the groups in these open sign-ups, so any open groups there aren't listed ` +
|
|
163
|
+
`(see unreadCategories): ${where}. Don't report them as full or closed; ` +
|
|
164
|
+
"check Groups > Available Groups in Brightspace.");
|
|
165
|
+
}
|
|
152
166
|
const data = {
|
|
153
167
|
coursesChecked: courses.length,
|
|
154
168
|
courses: withChoices,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.6",
|
|
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",
|