brightspace-mcp-server 4.0.5 → 4.0.7
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.
|
@@ -86,21 +86,23 @@ export function registerGetDropboxSubmissions(server, apiClient) {
|
|
|
86
86
|
const truncated = total > limit;
|
|
87
87
|
const kept = truncated ? rawSubmissions.slice(0, limit) : rawSubmissions;
|
|
88
88
|
// Optionally fetch feedback for each kept submitter, a bounded
|
|
89
|
-
// number at a time.
|
|
90
|
-
// simply has none in the map.
|
|
89
|
+
// number at a time. A 404 means no feedback yet, so that submitter
|
|
90
|
+
// simply has none in the map. Any other failure leaves their grading
|
|
91
|
+
// status unknown rather than "not graded", and the note names them.
|
|
91
92
|
const feedbackMap = new Map();
|
|
93
|
+
const feedbackUnread = new Set();
|
|
92
94
|
if (!ignoreFeedback) {
|
|
93
95
|
await mapWithConcurrency(kept, FEEDBACK_FETCH_CONCURRENCY, async (sub) => {
|
|
96
|
+
const userId = sub.SubmittedBy.Identifier;
|
|
94
97
|
try {
|
|
95
|
-
const userId = sub.SubmittedBy.Identifier;
|
|
96
98
|
const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/user/${userId}`);
|
|
97
99
|
const fb = await apiClient.get(feedbackPath);
|
|
98
100
|
feedbackMap.set(userId, fb);
|
|
99
101
|
}
|
|
100
|
-
catch {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
104
|
+
feedbackUnread.add(userId);
|
|
105
|
+
}
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
108
|
}
|
|
@@ -125,21 +127,41 @@ export function registerGetDropboxSubmissions(server, apiClient) {
|
|
|
125
127
|
comment: sub.Comment?.Text ?? null,
|
|
126
128
|
};
|
|
127
129
|
if (!ignoreFeedback) {
|
|
128
|
-
entry.feedbackStatus =
|
|
129
|
-
?
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
entry.feedbackStatus = feedbackUnread.has(userId)
|
|
131
|
+
? null
|
|
132
|
+
: feedback
|
|
133
|
+
? {
|
|
134
|
+
isGraded: feedback.IsGraded,
|
|
135
|
+
score: feedback.Score,
|
|
136
|
+
maxScore,
|
|
137
|
+
hasFeedbackText: !!feedback.Feedback?.Text,
|
|
138
|
+
}
|
|
139
|
+
: { isGraded: false, score: null, maxScore, hasFeedbackText: false };
|
|
136
140
|
}
|
|
137
141
|
return entry;
|
|
138
142
|
});
|
|
139
|
-
// activeOnly: skip submissions that are already fully graded.
|
|
143
|
+
// activeOnly: skip submissions that are already fully graded. One
|
|
144
|
+
// whose feedback couldn't be read stays, since it may still need grading.
|
|
140
145
|
const filtered = activeOnly && !ignoreFeedback
|
|
141
146
|
? submissions.filter((s) => !s.feedbackStatus?.isGraded)
|
|
142
147
|
: submissions;
|
|
148
|
+
const unreadNames = kept
|
|
149
|
+
.filter((sub) => feedbackUnread.has(sub.SubmittedBy.Identifier))
|
|
150
|
+
.map((sub) => `"${sub.SubmittedBy.DisplayName || sub.SubmittedBy.Identifier}"`);
|
|
151
|
+
if (unreadNames.length > 0) {
|
|
152
|
+
log("INFO", "get_dropbox_submissions: Some submitters' feedback could not be read", {
|
|
153
|
+
courseId,
|
|
154
|
+
folderId,
|
|
155
|
+
userIds: [...feedbackUnread],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const notes = [
|
|
159
|
+
...(truncated ? [`Showing ${kept.length} of ${total} submissions. Raise the limit argument to see more.`] : []),
|
|
160
|
+
...(unreadNames.length > 0
|
|
161
|
+
? [`Couldn't read the feedback for ${unreadNames.join(", ")}, so whether they are graded is unknown ` +
|
|
162
|
+
"(feedbackStatus is null)."]
|
|
163
|
+
: []),
|
|
164
|
+
];
|
|
143
165
|
log("INFO", `get_dropbox_submissions: ${filtered.length} submissions (of ${total} total) for folder ${folderId}`, { truncated });
|
|
144
166
|
return toolResponse({
|
|
145
167
|
courseId,
|
|
@@ -150,9 +172,7 @@ export function registerGetDropboxSubmissions(server, apiClient) {
|
|
|
150
172
|
totalSubmissions: total,
|
|
151
173
|
returnedSubmissions: filtered.length,
|
|
152
174
|
truncated,
|
|
153
|
-
...(
|
|
154
|
-
? { note: `Showing ${kept.length} of ${total} submissions. Raise the limit argument to see more.` }
|
|
155
|
-
: {}),
|
|
175
|
+
...(notes.length > 0 ? { note: notes.join(" ") } : {}),
|
|
156
176
|
activeOnly,
|
|
157
177
|
submissions: filtered,
|
|
158
178
|
});
|
|
@@ -55,14 +55,20 @@ export function registerGetDropboxUserSubmissions(server, apiClient) {
|
|
|
55
55
|
note: "No submissions found for this user in the specified folder.",
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
// A 404 means no feedback yet, matching the "not graded yet" default
|
|
59
|
+
// below. Any other failure leaves the grading status unknown.
|
|
58
60
|
let feedback = null;
|
|
61
|
+
let feedbackUnread = false;
|
|
59
62
|
if (!ignoreFeedback) {
|
|
60
63
|
try {
|
|
61
64
|
const feedbackPath = apiClient.le(courseId, `/dropbox/folders/${folderId}/feedback/user/${userId}`);
|
|
62
65
|
feedback = await apiClient.get(feedbackPath);
|
|
63
66
|
}
|
|
64
|
-
catch {
|
|
65
|
-
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
69
|
+
feedbackUnread = true;
|
|
70
|
+
log("INFO", "get_dropbox_user_submissions: Feedback could not be read", { courseId, folderId, userId });
|
|
71
|
+
}
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
const submissions = rawSubmissions.map((sub) => ({
|
|
@@ -79,14 +85,16 @@ export function registerGetDropboxUserSubmissions(server, apiClient) {
|
|
|
79
85
|
}));
|
|
80
86
|
const feedbackStatus = ignoreFeedback
|
|
81
87
|
? undefined
|
|
82
|
-
:
|
|
83
|
-
?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
: feedbackUnread
|
|
89
|
+
? null
|
|
90
|
+
: feedback
|
|
91
|
+
? {
|
|
92
|
+
isGraded: feedback.IsGraded,
|
|
93
|
+
score: feedback.Score,
|
|
94
|
+
hasFeedbackText: !!feedback.Feedback?.Text,
|
|
95
|
+
feedbackText: feedback.Feedback?.Text ?? null,
|
|
96
|
+
}
|
|
97
|
+
: { isGraded: false, score: null, hasFeedbackText: false, feedbackText: null };
|
|
90
98
|
log("INFO", `get_dropbox_user_submissions: ${submissions.length} submissions for user ${userId} in folder ${folderId}`);
|
|
91
99
|
return toolResponse({
|
|
92
100
|
courseId,
|
|
@@ -94,6 +102,9 @@ export function registerGetDropboxUserSubmissions(server, apiClient) {
|
|
|
94
102
|
userId,
|
|
95
103
|
submissions,
|
|
96
104
|
feedbackStatus,
|
|
105
|
+
...(feedbackUnread
|
|
106
|
+
? { note: "Couldn't read this user's feedback, so whether this submission is graded is unknown (feedbackStatus is null)." }
|
|
107
|
+
: {}),
|
|
97
108
|
});
|
|
98
109
|
}
|
|
99
110
|
catch (error) {
|
|
@@ -22,8 +22,9 @@ const QUIZ_FEEDBACK_NOTE = "The gradebook has no comment for this item, but it i
|
|
|
22
22
|
"browser such as Respondus LockDown Browser. Open feedbackUrl to check; do not report " +
|
|
23
23
|
"this item as having no feedback.";
|
|
24
24
|
/**
|
|
25
|
-
* The course's quizzes, or
|
|
26
|
-
* quiz list only costs the feedback links, never the grades themselves
|
|
25
|
+
* The course's quizzes, or null when the route refuses or fails. A missing
|
|
26
|
+
* quiz list only costs the feedback links, never the grades themselves, but
|
|
27
|
+
* the caller is told so a gradebook link isn't read as "not a quiz" (#278).
|
|
27
28
|
*/
|
|
28
29
|
async function fetchQuizzes(apiClient, courseId) {
|
|
29
30
|
try {
|
|
@@ -34,7 +35,7 @@ async function fetchQuizzes(apiClient, courseId) {
|
|
|
34
35
|
}
|
|
35
36
|
catch (error) {
|
|
36
37
|
log("DEBUG", `get_my_grades: quiz list unavailable for course ${courseId}`, error);
|
|
37
|
-
return
|
|
38
|
+
return null;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
@@ -42,7 +43,7 @@ async function fetchQuizzes(apiClient, courseId) {
|
|
|
42
43
|
* rows keep the quiz source URL and note; other rows link to the gradebook.
|
|
43
44
|
*/
|
|
44
45
|
function toGradeItems(gradeValues, quizzes, baseUrl, courseId) {
|
|
45
|
-
const quizByGradeItem = new Map(quizzes
|
|
46
|
+
const quizByGradeItem = new Map((quizzes ?? [])
|
|
46
47
|
.filter((q) => q.GradeItemId != null)
|
|
47
48
|
.map((q) => [String(q.GradeItemId), q.QuizId]));
|
|
48
49
|
return gradeValues.map((gv) => {
|
|
@@ -65,6 +66,11 @@ function toGradeItems(gradeValues, quizzes, baseUrl, courseId) {
|
|
|
65
66
|
};
|
|
66
67
|
});
|
|
67
68
|
}
|
|
69
|
+
function quizzesUnavailableNotice(courseIds) {
|
|
70
|
+
return (`The quiz list for ${courseIds.length} course(s) (${courseIds.join(", ")}) could not be read, so ` +
|
|
71
|
+
"quiz-scored grade items there link to the gradebook and lack the quiz feedback note. Quiz " +
|
|
72
|
+
"feedback may still exist on the quiz's own page; do not report those items as having no feedback.");
|
|
73
|
+
}
|
|
68
74
|
/**
|
|
69
75
|
* Register get_my_grades tool
|
|
70
76
|
*/
|
|
@@ -122,8 +128,17 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
122
128
|
throw error;
|
|
123
129
|
}
|
|
124
130
|
// Map to clean objects
|
|
125
|
-
const
|
|
131
|
+
const quizzes = await quizzesPromise;
|
|
132
|
+
const grades = toGradeItems(gradeValues, quizzes, config.baseUrl, courseId);
|
|
126
133
|
log("INFO", `get_my_grades: Retrieved ${grades.length} grade items for course ${courseId}`);
|
|
134
|
+
if (quizzes === null) {
|
|
135
|
+
return toolResponse({
|
|
136
|
+
courseId,
|
|
137
|
+
grades,
|
|
138
|
+
quizzesUnavailable: true,
|
|
139
|
+
notice: quizzesUnavailableNotice([courseId]),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
127
142
|
return toolResponse({ courseId, grades });
|
|
128
143
|
}
|
|
129
144
|
// All courses case
|
|
@@ -160,6 +175,7 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
160
175
|
courseId: item.OrgUnit.Id,
|
|
161
176
|
courseName: item.OrgUnit.Name,
|
|
162
177
|
grades,
|
|
178
|
+
...(quizzes === null ? { quizzesUnavailable: true } : {}),
|
|
163
179
|
};
|
|
164
180
|
}
|
|
165
181
|
catch (error) {
|
|
@@ -216,7 +232,12 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
216
232
|
.map(({ failed: _failed, ...rest }) => rest);
|
|
217
233
|
const courses = settled
|
|
218
234
|
.filter((c) => !c.authPending && !c.restricted && !c.failed)
|
|
219
|
-
.map(({ courseId, courseName, grades }) => ({
|
|
235
|
+
.map(({ courseId, courseName, grades, quizzesUnavailable }) => ({
|
|
236
|
+
courseId,
|
|
237
|
+
courseName,
|
|
238
|
+
grades,
|
|
239
|
+
...(quizzesUnavailable ? { quizzesUnavailable } : {}),
|
|
240
|
+
}));
|
|
220
241
|
log("INFO", `get_my_grades: Retrieved grades for ${courses.length} courses, ` +
|
|
221
242
|
`${restrictedCourses.length} restricted (out of ${enrollmentItems.length} enrolled` +
|
|
222
243
|
`${pending.length > 0 ? `, ${pending.length} pending sign-in` : ""}` +
|
|
@@ -255,6 +276,9 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
255
276
|
"so they are listed in failedCourses instead of courses. Do not report these courses as " +
|
|
256
277
|
"having no grades: open each gradeUrl, or call get_my_grades again for that courseId.");
|
|
257
278
|
}
|
|
279
|
+
const quizGaps = courses.filter((c) => c.quizzesUnavailable).map((c) => c.courseId);
|
|
280
|
+
if (quizGaps.length > 0)
|
|
281
|
+
notices.push(quizzesUnavailableNotice(quizGaps));
|
|
258
282
|
if (notices.length > 0)
|
|
259
283
|
response.notice = notices.join(" ");
|
|
260
284
|
return toolResponse(response);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.7",
|
|
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",
|