brightspace-mcp-server 3.9.12 → 3.9.14
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.
package/build/auth/purdue-sso.js
CHANGED
|
@@ -295,11 +295,18 @@ export class PurdueSSOFlow {
|
|
|
295
295
|
return await this.hasCoVisiblePassword(page) && !await this.anyVisible(page, EMAIL_SELECTORS);
|
|
296
296
|
}
|
|
297
297
|
async hasPostCredentialChallenge(page) {
|
|
298
|
+
// Entra can land directly on its bare verification-code form - no
|
|
299
|
+
// number-match digits, no #idDiv_SAOTCAS_Title/#idDiv_SAOTCC_Title
|
|
300
|
+
// heading - when resuming a restored session. Without MFA_CODE_SELECTORS
|
|
301
|
+
// here, login() concluded no challenge was present and re-ran
|
|
302
|
+
// enterCredentials against a page with no username/password field.
|
|
303
|
+
// Idea from ElliotDrel/brightspace-mcp (branch codex/purdue-totp).
|
|
298
304
|
return this.duoMfa.isChallenge(page) || await this.anyVisible(page, [
|
|
299
305
|
NUMBER_MATCH_SELECTOR,
|
|
300
306
|
"#idDiv_SAOTCAS_Title",
|
|
301
307
|
"#idDiv_SAOTCC_Title",
|
|
302
308
|
"#KmsiCheckboxField",
|
|
309
|
+
...MFA_CODE_SELECTORS,
|
|
303
310
|
]);
|
|
304
311
|
}
|
|
305
312
|
/** Brightspace Bar's bounded number/auth/KMSI polling loop. */
|
|
@@ -156,6 +156,16 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
|
156
156
|
log("DEBUG", `Failed to fetch feedback for folder ${folder.Id}`, error);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
// Instructor-provided URL links attached to the folder. Omitted
|
|
160
|
+
// entirely (rather than an empty array) when the folder has none, to
|
|
161
|
+
// keep the common case's shape exactly as it was.
|
|
162
|
+
// Adapted from LunaParker/brightspace-mcp-server (MIT).
|
|
163
|
+
// D2L's field for the link's display text is `LinkName`; `Title` is
|
|
164
|
+
// kept as a fallback for tenants observed sending that spelling
|
|
165
|
+
// instead. Adapted from JhostinAleck/brightspace-mcp (MIT).
|
|
166
|
+
const linkAttachments = (folder.LinkAttachments ?? [])
|
|
167
|
+
.filter((l) => Boolean(l.Href))
|
|
168
|
+
.map((l) => ({ name: l.LinkName ?? l.Title ?? l.Href ?? null, url: l.Href }));
|
|
159
169
|
// Build assignment object
|
|
160
170
|
const assignment = {
|
|
161
171
|
type: "assignment",
|
|
@@ -169,6 +179,7 @@ export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
|
169
179
|
dueIn: dueIn(folder.DueDate),
|
|
170
180
|
points: folder.Assessment?.ScoreDenominator ?? null,
|
|
171
181
|
isGroup: folder.GroupTypeId !== null,
|
|
182
|
+
...(linkAttachments.length > 0 ? { linkAttachments } : {}),
|
|
172
183
|
rubric: folder.Assessment?.Rubrics?.map((r) => ({
|
|
173
184
|
name: r.Name,
|
|
174
185
|
criteria: r.Criteria?.map((c) => ({
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
import { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
7
7
|
import { fetchAllItems } from "../api/paginate.js";
|
|
8
8
|
import { GetMyGradesSchema, } from "./schemas.js";
|
|
9
|
-
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
9
|
+
import { toolResponse, sanitizeError, errorResponse } from "./tool-helpers.js";
|
|
10
10
|
import { log } from "../utils/logger.js";
|
|
11
11
|
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
12
|
+
import { gradebookUrl } from "../utils/deep-links.js";
|
|
12
13
|
/**
|
|
13
14
|
* Register get_my_grades tool
|
|
14
15
|
*/
|
|
@@ -25,9 +26,27 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
25
26
|
// Single course case
|
|
26
27
|
if (courseId) {
|
|
27
28
|
const path = apiClient.le(courseId, "/grades/values/myGradeValues/");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
let gradeValues;
|
|
30
|
+
try {
|
|
31
|
+
gradeValues = await apiClient.get(path, {
|
|
32
|
+
ttl: DEFAULT_CACHE_TTLS.grades,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// A 403 here means the tenant restricts the grade API for this
|
|
37
|
+
// course (an institutional policy, not a bug). The error shape
|
|
38
|
+
// stays the same plain isError text result every other tool
|
|
39
|
+
// error uses -- only the text itself changes, naming the
|
|
40
|
+
// gradebook URL so the student can see their grade there
|
|
41
|
+
// instead.
|
|
42
|
+
if (error?.status === 403) {
|
|
43
|
+
const url = gradebookUrl(config.baseUrl, courseId);
|
|
44
|
+
log("INFO", `get_my_grades: 403 for course ${courseId} - grade API access restricted`);
|
|
45
|
+
return errorResponse("Your institution restricts grade API access for this course. " +
|
|
46
|
+
`View your grades directly in Brightspace: ${url}`);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
31
50
|
// Map to clean objects
|
|
32
51
|
const grades = gradeValues.map((gv) => ({
|
|
33
52
|
name: gv.GradeObjectName,
|
|
@@ -78,26 +97,43 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
78
97
|
lastModified: gv.LastModified,
|
|
79
98
|
}));
|
|
80
99
|
return {
|
|
100
|
+
restricted: false,
|
|
81
101
|
courseId: item.OrgUnit.Id,
|
|
82
102
|
courseName: item.OrgUnit.Name,
|
|
83
103
|
grades,
|
|
84
104
|
};
|
|
85
105
|
}
|
|
86
106
|
catch (error) {
|
|
87
|
-
// 403 means
|
|
107
|
+
// A 403 means the tenant restricts the grade API for this course
|
|
108
|
+
// (an institutional policy, not missing access like a dropped
|
|
109
|
+
// past course). Rather than dropping the course with no trace,
|
|
110
|
+
// it is surfaced in a separate, purely additive
|
|
111
|
+
// `restrictedCourses` array; `courses` itself is unaffected.
|
|
88
112
|
if (error?.status === 403) {
|
|
89
|
-
log("DEBUG", `get_my_grades: 403 Forbidden for course ${item.OrgUnit.Id} (${item.OrgUnit.Name}) -
|
|
90
|
-
return
|
|
113
|
+
log("DEBUG", `get_my_grades: 403 Forbidden for course ${item.OrgUnit.Id} (${item.OrgUnit.Name}) - grade API access restricted`);
|
|
114
|
+
return {
|
|
115
|
+
restricted: true,
|
|
116
|
+
courseId: item.OrgUnit.Id,
|
|
117
|
+
courseName: item.OrgUnit.Name,
|
|
118
|
+
gradeUrl: gradebookUrl(config.baseUrl, item.OrgUnit.Id),
|
|
119
|
+
};
|
|
91
120
|
}
|
|
92
121
|
throw error; // Re-throw other errors
|
|
93
122
|
}
|
|
94
123
|
});
|
|
95
124
|
const results = await Promise.allSettled(gradePromises);
|
|
96
|
-
const
|
|
97
|
-
.filter((r) => r.status === "fulfilled"
|
|
125
|
+
const settled = results
|
|
126
|
+
.filter((r) => r.status === "fulfilled")
|
|
98
127
|
.map((r) => r.value);
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
const courses = settled
|
|
129
|
+
.filter((r) => !r.restricted)
|
|
130
|
+
.map(({ restricted: _restricted, ...rest }) => rest);
|
|
131
|
+
const restrictedCourses = settled
|
|
132
|
+
.filter((r) => r.restricted)
|
|
133
|
+
.map(({ restricted: _restricted, ...rest }) => rest);
|
|
134
|
+
log("INFO", `get_my_grades: Retrieved grades for ${courses.length} courses, ` +
|
|
135
|
+
`${restrictedCourses.length} restricted (out of ${enrollmentItems.length} enrolled)`);
|
|
136
|
+
return toolResponse({ courses, restrictedCourses });
|
|
101
137
|
}
|
|
102
138
|
catch (error) {
|
|
103
139
|
return sanitizeError(error);
|
|
@@ -85,6 +85,11 @@ export const ALLOWED_MIME_TYPES = [
|
|
|
85
85
|
// Archives
|
|
86
86
|
"application/zip",
|
|
87
87
|
"application/x-zip-compressed",
|
|
88
|
+
"application/x-7z-compressed",
|
|
89
|
+
"application/gzip",
|
|
90
|
+
"application/x-gzip",
|
|
91
|
+
"application/x-tar",
|
|
92
|
+
"application/x-bzip2",
|
|
88
93
|
// Media
|
|
89
94
|
"video/mp4",
|
|
90
95
|
"audio/mpeg",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.14",
|
|
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",
|