brightspace-mcp-server 3.11.9 → 3.11.11
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.
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { ApiError, DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
7
|
import { GetAssignmentRubricSchema } from "./schemas.js";
|
|
8
8
|
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
9
|
-
import {
|
|
9
|
+
import { feedbackText } from "../utils/feedback.js";
|
|
10
10
|
import { log } from "../utils/logger.js";
|
|
11
11
|
// D2L's documented SCORING_M enumeration
|
|
12
12
|
// (https://docs.valence.desire2learn.com/res/assessment.html). Only values
|
|
@@ -20,10 +20,7 @@ const SCORING_METHOD_NAMES = {
|
|
|
20
20
|
};
|
|
21
21
|
/** Rich text to plain markdown, or undefined when there is nothing to say. */
|
|
22
22
|
function textOf(rt) {
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
const text = rt.Html ? convertHtmlToMarkdown(rt.Html).markdown : rt.Text ?? undefined;
|
|
26
|
-
return text && text.trim().length > 0 ? text : undefined;
|
|
23
|
+
return feedbackText(rt) ?? undefined;
|
|
27
24
|
}
|
|
28
25
|
/** The groups a rubric carries, normalizing the grouped and flattened shapes. */
|
|
29
26
|
function groupsOf(rubric) {
|
|
@@ -120,7 +117,7 @@ function levelNamesOf(rubric) {
|
|
|
120
117
|
* The student's own graded outcome on this rubric, resolved against the
|
|
121
118
|
* rubric's own criterion/level names rather than re-sent by the assessment.
|
|
122
119
|
*/
|
|
123
|
-
function toOutputAssessment(rubric, assessment) {
|
|
120
|
+
export function toOutputAssessment(rubric, assessment) {
|
|
124
121
|
const criterionNames = criterionNamesOf(rubric);
|
|
125
122
|
const levelNames = levelNamesOf(rubric);
|
|
126
123
|
const out = { criteria: [] };
|
|
@@ -13,6 +13,8 @@ import { applyCourseFilter } from "../utils/course-filter.js";
|
|
|
13
13
|
import { gradebookUrl, quizUrl } from "../utils/deep-links.js";
|
|
14
14
|
import { assignmentLinkResolver } from "./assignment-links.js";
|
|
15
15
|
import { dueIn } from "../utils/due-in.js";
|
|
16
|
+
import { feedbackText, FEEDBACK_UNAVAILABLE_NOTE } from "../utils/feedback.js";
|
|
17
|
+
import { toOutputAssessment } from "./get-assignment-rubric.js";
|
|
16
18
|
/** The display HTML of a quiz rich-text field, nested shape or flat. */
|
|
17
19
|
function richTextHtml(field) {
|
|
18
20
|
if (!field)
|
|
@@ -154,13 +156,18 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
|
154
156
|
}
|
|
155
157
|
// Fetch feedback independently of submissions
|
|
156
158
|
let feedback = null;
|
|
159
|
+
let feedbackStatus = "unavailable";
|
|
157
160
|
try {
|
|
158
161
|
feedback = await apiClient.get(apiClient.le(courseId, `/dropbox/folders/${folder.Id}/feedback/myFeedback/`), { ttl: DEFAULT_CACHE_TTLS.assignments });
|
|
162
|
+
feedbackStatus = feedback ? "retrieved" : "unavailable";
|
|
159
163
|
}
|
|
160
164
|
catch (error) {
|
|
161
165
|
if (isAuthUnavailable(error))
|
|
162
166
|
throw error;
|
|
163
|
-
//
|
|
167
|
+
// Permission failures, missing API records, and transport failures
|
|
168
|
+
// do not establish whether published feedback exists in the UI.
|
|
169
|
+
feedbackStatus = error?.status === 403 ? "restricted"
|
|
170
|
+
: error?.status === 404 ? "unavailable" : "error";
|
|
164
171
|
if (error?.status !== 404 && error?.status !== 403) {
|
|
165
172
|
log("DEBUG", `Failed to fetch feedback for folder ${folder.Id}`, error);
|
|
166
173
|
}
|
|
@@ -176,6 +183,15 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
|
176
183
|
.filter((l) => Boolean(l.Href))
|
|
177
184
|
.map((l) => ({ name: l.LinkName ?? l.Title ?? l.Href ?? null, url: l.Href }));
|
|
178
185
|
// Build assignment object
|
|
186
|
+
const overallFeedback = feedbackText(feedback?.Feedback);
|
|
187
|
+
const rubricAssessments = (feedback?.RubricAssessments ?? []).map((assessment) => {
|
|
188
|
+
const rubric = folder.Assessment?.Rubrics?.find((r) => r.RubricId === assessment.RubricId);
|
|
189
|
+
return {
|
|
190
|
+
rubricId: assessment.RubricId,
|
|
191
|
+
...toOutputAssessment((rubric ?? { RubricId: assessment.RubricId, Name: "" }), assessment),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
const commentsRetrieved = Boolean(overallFeedback || rubricAssessments.some((rubric) => rubric.feedback || rubric.criteria.some((criterion) => criterion.feedback)));
|
|
179
195
|
const assignment = {
|
|
180
196
|
type: "assignment",
|
|
181
197
|
id: folder.Id,
|
|
@@ -227,11 +243,14 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
|
227
243
|
feedback: feedback
|
|
228
244
|
? {
|
|
229
245
|
score: feedback.Score,
|
|
230
|
-
feedback:
|
|
231
|
-
|
|
232
|
-
: null,
|
|
246
|
+
feedback: overallFeedback,
|
|
247
|
+
rubricAssessments,
|
|
233
248
|
}
|
|
234
249
|
: null,
|
|
250
|
+
feedbackStatus,
|
|
251
|
+
feedbackCommentsStatus: commentsRetrieved ? "retrieved" : "unavailable",
|
|
252
|
+
feedbackUrl: baseUrl ? gradebookUrl(baseUrl, courseId) : null,
|
|
253
|
+
...(commentsRetrieved ? {} : { feedbackStatusNote: FEEDBACK_UNAVAILABLE_NOTE }),
|
|
235
254
|
};
|
|
236
255
|
assignments.push(assignment);
|
|
237
256
|
}
|
|
@@ -10,6 +10,7 @@ import { toolResponse, sanitizeError, errorResponse, isAuthUnavailable, authPend
|
|
|
10
10
|
import { log } from "../utils/logger.js";
|
|
11
11
|
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
12
12
|
import { gradebookUrl, quizUrl } from "../utils/deep-links.js";
|
|
13
|
+
import { feedbackText, FEEDBACK_UNAVAILABLE_NOTE } from "../utils/feedback.js";
|
|
13
14
|
/**
|
|
14
15
|
* The quiz a grade row is scored from can hold feedback the gradebook does
|
|
15
16
|
* not, and the student API cannot read it: quiz attempts are refused to
|
|
@@ -35,15 +36,15 @@ async function fetchQuizzes(apiClient, courseId) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Normalize public comments and report their retrieval status. Quiz-linked
|
|
40
|
+
* rows keep the quiz source URL and note; other rows link to the gradebook.
|
|
40
41
|
*/
|
|
41
42
|
function toGradeItems(gradeValues, quizzes, baseUrl, courseId) {
|
|
42
43
|
const quizByGradeItem = new Map(quizzes
|
|
43
44
|
.filter((q) => q.GradeItemId != null)
|
|
44
45
|
.map((q) => [String(q.GradeItemId), q.QuizId]));
|
|
45
46
|
return gradeValues.map((gv) => {
|
|
46
|
-
const comments = gv.Comments
|
|
47
|
+
const comments = feedbackText(gv.Comments);
|
|
47
48
|
const quizId = quizByGradeItem.get(String(gv.GradeObjectIdentifier));
|
|
48
49
|
return {
|
|
49
50
|
name: gv.GradeObjectName,
|
|
@@ -52,12 +53,13 @@ function toGradeItems(gradeValues, quizzes, baseUrl, courseId) {
|
|
|
52
53
|
pointsDenominator: gv.PointsDenominator,
|
|
53
54
|
weightedNumerator: gv.WeightedNumerator,
|
|
54
55
|
weightedDenominator: gv.WeightedDenominator,
|
|
56
|
+
gradeObjectId: gv.GradeObjectIdentifier,
|
|
55
57
|
comments,
|
|
58
|
+
feedbackStatus: comments ? "retrieved" : "unavailable",
|
|
59
|
+
feedbackUrl: quizId === undefined ? gradebookUrl(baseUrl, courseId) : quizUrl(baseUrl, courseId, quizId),
|
|
60
|
+
...(comments ? {} : { feedbackStatusNote: FEEDBACK_UNAVAILABLE_NOTE }),
|
|
56
61
|
lastModified: gv.LastModified,
|
|
57
|
-
...(quizId
|
|
58
|
-
feedbackUrl: quizUrl(baseUrl, courseId, quizId),
|
|
59
|
-
...(comments ? {} : { feedbackNote: QUIZ_FEEDBACK_NOTE }),
|
|
60
|
-
}),
|
|
62
|
+
...(quizId !== undefined && !comments ? { feedbackNote: QUIZ_FEEDBACK_NOTE } : {}),
|
|
61
63
|
};
|
|
62
64
|
});
|
|
63
65
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Public instructor feedback only; never substitute private comments. */
|
|
2
|
+
import { convertHtmlToMarkdown } from "./html-converter.js";
|
|
3
|
+
export function feedbackText(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return null;
|
|
6
|
+
const markdown = value.Html ? convertHtmlToMarkdown(value.Html).markdown : null;
|
|
7
|
+
if (markdown?.trim())
|
|
8
|
+
return markdown;
|
|
9
|
+
return value.Text?.trim() ? value.Text : null;
|
|
10
|
+
}
|
|
11
|
+
export const FEEDBACK_UNAVAILABLE_NOTE = "Brightspace did not return instructor comments through this API. Published feedback may still " +
|
|
12
|
+
"be available in Brightspace; do not interpret null as no feedback. Check the source URL.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.11",
|
|
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",
|