brightspace-mcp-server 5.2.5 → 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
- // Log dropbox fetch failure but don't throw
637
- log("DEBUG", `Failed to fetch dropbox folders for course ${courseId}`, dropboxResult.reason);
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; // Re-throw other errors
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" && r.value !== null)
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
- response.notice =
998
- "Sign-in to Brightspace is still in progress, so assignments for " +
999
- `${pending.length} course(s) (${pending.map((c) => c.courseId).join(", ")}) could not be ` +
1000
- `fetched yet. ${authPendingNotice(pending[0].authError)} Call get_assignments again once ` +
1001
- "sign-in finishes.";
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.2.5",
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",