brightspace-mcp-server 5.1.27 → 5.1.28
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.
|
@@ -13,6 +13,15 @@ import { assignmentLinkResolver } from "./assignment-links.js";
|
|
|
13
13
|
import { dueIn } from "../utils/due-in.js";
|
|
14
14
|
import { resolveCourses } from "./resolve-courses.js";
|
|
15
15
|
import { fetchCourseCalendarEvents, eventEndsAt } from "./calendar-events.js";
|
|
16
|
+
/**
|
|
17
|
+
* Whether a failed source must be reported rather than skipped. A 403 means
|
|
18
|
+
* the tool is restricted or switched off in that course (a past course, a
|
|
19
|
+
* course with no quizzes): nothing was missed. Anything else (a 404, a 5xx, a
|
|
20
|
+
* dropped connection) leaves a hole the caller would read as "nothing due".
|
|
21
|
+
*/
|
|
22
|
+
function isReportableFailure(error) {
|
|
23
|
+
return error?.status !== 403;
|
|
24
|
+
}
|
|
16
25
|
/** D2L list endpoints return either a paged { Objects: [...] } or a flat array. */
|
|
17
26
|
function unwrapList(raw) {
|
|
18
27
|
return Array.isArray(raw) ? raw : (raw?.Objects ?? []);
|
|
@@ -24,11 +33,13 @@ function unwrapList(raw) {
|
|
|
24
33
|
* hides everything inside it, however visible its topics claim to be, so it is
|
|
25
34
|
* skipped without asking for its topics at all. A forum whose topics fail to
|
|
26
35
|
* load (e.g. no access) is skipped rather than failing the whole course,
|
|
27
|
-
* matching `getForumsOverview` in get-discussions.ts
|
|
36
|
+
* matching `getForumsOverview` in get-discussions.ts; `incomplete` marks a
|
|
37
|
+
* skip that was not a 403, so the caller can say topics may be missing.
|
|
28
38
|
*/
|
|
29
39
|
async function fetchDiscussionDueTopics(apiClient, courseId) {
|
|
30
40
|
const forums = await apiClient.get(apiClient.le(courseId, "/discussions/forums/"), { ttl: DEFAULT_CACHE_TTLS.assignments });
|
|
31
41
|
const topics = [];
|
|
42
|
+
let incomplete = false;
|
|
32
43
|
for (const forum of unwrapList(forums)) {
|
|
33
44
|
if (forum.IsHidden === true)
|
|
34
45
|
continue;
|
|
@@ -39,10 +50,12 @@ async function fetchDiscussionDueTopics(apiClient, courseId) {
|
|
|
39
50
|
catch (error) {
|
|
40
51
|
if (isAuthUnavailable(error))
|
|
41
52
|
throw error;
|
|
53
|
+
if (isReportableFailure(error))
|
|
54
|
+
incomplete = true;
|
|
42
55
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch topics for forum ${forum.ForumId} in course ${courseId}`, error);
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
|
-
return topics;
|
|
58
|
+
return { topics, incomplete };
|
|
46
59
|
}
|
|
47
60
|
/**
|
|
48
61
|
* Calendar events as upcoming items, minus the ones Brightspace generated from
|
|
@@ -94,6 +107,7 @@ async function fetchCourseDueItems(apiClient, baseUrl, course, from, to) {
|
|
|
94
107
|
throw result.reason;
|
|
95
108
|
}
|
|
96
109
|
const items = [];
|
|
110
|
+
const unavailable = [];
|
|
97
111
|
const assignmentLink = assignmentLinkResolver(apiClient, baseUrl, course.id);
|
|
98
112
|
if (dropboxResult.status === "fulfilled") {
|
|
99
113
|
for (const folder of unwrapList(dropboxResult.value)) {
|
|
@@ -125,6 +139,8 @@ async function fetchCourseDueItems(apiClient, baseUrl, course, from, to) {
|
|
|
125
139
|
}
|
|
126
140
|
else {
|
|
127
141
|
// 403 for past courses and similar: log and keep the other courses.
|
|
142
|
+
if (isReportableFailure(dropboxResult.reason))
|
|
143
|
+
unavailable.push("assignments");
|
|
128
144
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch dropbox folders for course ${course.id}`, dropboxResult.reason);
|
|
129
145
|
}
|
|
130
146
|
if (quizResult.status === "fulfilled") {
|
|
@@ -150,10 +166,14 @@ async function fetchCourseDueItems(apiClient, baseUrl, course, from, to) {
|
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
else {
|
|
169
|
+
if (isReportableFailure(quizResult.reason))
|
|
170
|
+
unavailable.push("quizzes");
|
|
153
171
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch quizzes for course ${course.id}`, quizResult.reason);
|
|
154
172
|
}
|
|
155
173
|
if (discussionResult.status === "fulfilled") {
|
|
156
|
-
|
|
174
|
+
if (discussionResult.value.incomplete)
|
|
175
|
+
unavailable.push("discussions");
|
|
176
|
+
for (const topic of discussionResult.value.topics) {
|
|
157
177
|
if (topic.IsHidden === true)
|
|
158
178
|
continue;
|
|
159
179
|
if (!topic.DueDate)
|
|
@@ -173,15 +193,19 @@ async function fetchCourseDueItems(apiClient, baseUrl, course, from, to) {
|
|
|
173
193
|
}
|
|
174
194
|
}
|
|
175
195
|
else {
|
|
196
|
+
if (isReportableFailure(discussionResult.reason))
|
|
197
|
+
unavailable.push("discussions");
|
|
176
198
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch discussions for course ${course.id}`, discussionResult.reason);
|
|
177
199
|
}
|
|
178
200
|
if (calendarResult.status === "fulfilled") {
|
|
179
201
|
items.push(...calendarItems(calendarResult.value, items));
|
|
180
202
|
}
|
|
181
203
|
else {
|
|
204
|
+
if (isReportableFailure(calendarResult.reason))
|
|
205
|
+
unavailable.push("calendar");
|
|
182
206
|
log("DEBUG", `get_upcoming_due_dates: failed to fetch calendar events for course ${course.id}`, calendarResult.reason);
|
|
183
207
|
}
|
|
184
|
-
return items;
|
|
208
|
+
return { items, unavailable };
|
|
185
209
|
}
|
|
186
210
|
/**
|
|
187
211
|
* Register get_upcoming_due_dates tool
|
|
@@ -209,16 +233,28 @@ export function registerGetUpcomingDueDates(server, apiClient, config) {
|
|
|
209
233
|
// have gone out independently. Collect it rather than failing the
|
|
210
234
|
// whole call and losing every course that *did* answer.
|
|
211
235
|
const pendingCourseIds = [];
|
|
236
|
+
// Anything else that failed still must not make items vanish: an
|
|
237
|
+
// assignment missing from a list that looks complete reads as
|
|
238
|
+
// "nothing due" (#383).
|
|
239
|
+
const failedCourses = [];
|
|
212
240
|
let firstAuthError = null;
|
|
213
241
|
const items = results.flatMap((result, i) => {
|
|
214
|
-
if (result.status === "fulfilled")
|
|
215
|
-
|
|
242
|
+
if (result.status === "fulfilled") {
|
|
243
|
+
if (result.value.unavailable.length > 0) {
|
|
244
|
+
failedCourses.push({ courseId: courses[i].id, sources: result.value.unavailable });
|
|
245
|
+
}
|
|
246
|
+
return result.value.items;
|
|
247
|
+
}
|
|
216
248
|
if (isAuthUnavailable(result.reason)) {
|
|
217
249
|
pendingCourseIds.push(courses[i].id);
|
|
218
250
|
firstAuthError ??= result.reason;
|
|
219
251
|
log("DEBUG", `get_upcoming_due_dates: sign-in pending for course ${courses[i].id}`, result.reason);
|
|
220
252
|
return [];
|
|
221
253
|
}
|
|
254
|
+
failedCourses.push({
|
|
255
|
+
courseId: courses[i].id,
|
|
256
|
+
sources: ["assignments", "quizzes", "discussions", "calendar"],
|
|
257
|
+
});
|
|
222
258
|
log("DEBUG", "get_upcoming_due_dates: skipping course after fetch failure", result.reason);
|
|
223
259
|
return [];
|
|
224
260
|
});
|
|
@@ -234,18 +270,30 @@ export function registerGetUpcomingDueDates(server, apiClient, config) {
|
|
|
234
270
|
})
|
|
235
271
|
.sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
|
|
236
272
|
log("INFO", `get_upcoming_due_dates: Retrieved ${upcoming.length} items across ${courses.length} courses` +
|
|
237
|
-
(pendingCourseIds.length > 0 ? ` (${pendingCourseIds.length} pending sign-in)` : "")
|
|
273
|
+
(pendingCourseIds.length > 0 ? ` (${pendingCourseIds.length} pending sign-in)` : "") +
|
|
274
|
+
(failedCourses.length > 0 ? ` (${failedCourses.length} incomplete)` : ""));
|
|
238
275
|
// The response is always a bare array, pending sign-in or not — a
|
|
239
|
-
// shape change would break every existing caller. A pending
|
|
240
|
-
// instead adds a second content block carrying the
|
|
241
|
-
// names which course ids are unavailable.
|
|
242
|
-
|
|
276
|
+
// shape change would break every existing caller. A pending or
|
|
277
|
+
// incomplete course instead adds a second content block carrying the
|
|
278
|
+
// notice, which names which course ids are unavailable.
|
|
279
|
+
const notices = [];
|
|
280
|
+
if (pendingCourseIds.length > 0) {
|
|
281
|
+
throwIfCredentialRejected(firstAuthError);
|
|
282
|
+
notices.push("Sign-in to Brightspace is still in progress, so upcoming due dates for " +
|
|
283
|
+
`${pendingCourseIds.length} course(s) (${pendingCourseIds.join(", ")}) could not be fetched ` +
|
|
284
|
+
`yet. ${authPendingNotice(firstAuthError)} Call get_upcoming_due_dates again once sign-in finishes.`);
|
|
285
|
+
}
|
|
286
|
+
if (failedCourses.length > 0) {
|
|
287
|
+
const listed = failedCourses
|
|
288
|
+
.map(({ courseId, sources }) => `${courseId} (${sources.join(", ")})`)
|
|
289
|
+
.join("; ");
|
|
290
|
+
notices.push(`Some upcoming items could not be read from Brightspace, so they may be missing from this list: ${listed}. ` +
|
|
291
|
+
"Do not report these as having nothing due: call get_upcoming_due_dates again with that courseId.");
|
|
292
|
+
}
|
|
293
|
+
if (notices.length === 0) {
|
|
243
294
|
return toolResponse(upcoming);
|
|
244
295
|
}
|
|
245
|
-
|
|
246
|
-
return toolResponseWithNotice(upcoming, "Sign-in to Brightspace is still in progress, so upcoming due dates for " +
|
|
247
|
-
`${pendingCourseIds.length} course(s) (${pendingCourseIds.join(", ")}) could not be fetched ` +
|
|
248
|
-
`yet. ${authPendingNotice(firstAuthError)} Call get_upcoming_due_dates again once sign-in finishes.`);
|
|
296
|
+
return toolResponseWithNotice(upcoming, notices.join(" "));
|
|
249
297
|
}
|
|
250
298
|
catch (error) {
|
|
251
299
|
return sanitizeError(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.28",
|
|
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",
|