brightspace-mcp-server 3.10.13 → 3.10.16
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/README.md +3 -2
- package/build/auth/auth-runner.js +27 -30
- package/build/tools/get-my-grades.js +59 -24
- package/build/tools/get-syllabus.js +51 -1
- package/build/tools/tool-helpers.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,12 +65,13 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
|
|
|
65
65
|
|
|
66
66
|
| About | Try |
|
|
67
67
|
|-------|-----|
|
|
68
|
-
| Grades | "Am I passing all my classes?" · "Compare my grades across courses" |
|
|
68
|
+
| Grades | "Am I passing all my classes?" · "Compare my grades across courses" · "Is there feedback on my midterm?" (quiz-scored grades link to the quiz, since quiz feedback, including feedback only viewable in LockDown Browser, isn't readable through the API) |
|
|
69
69
|
| Due dates | "What's due in the next 48 hours?" · "Build me a study schedule for the week" |
|
|
70
70
|
| Assignments and rubrics | "What does the lab 4 spec actually ask for?" · "Why did I lose points on the analysis criterion?" |
|
|
71
71
|
| Quizzes and exams | "Which quizzes close this week?" · "Is there a midterm in the gradebook that isn't on my assignments list?" |
|
|
72
72
|
| Announcements | "Did any professor post something important today?" · "Read the file attached to today's announcement" |
|
|
73
73
|
| Course content | "Find the midterm review slides" · "Download every PDF from Module 5" · "Search this course for office hours" · or just read a file inline instead of saving it |
|
|
74
|
+
| Syllabus | "What's the late policy in ECE 264's syllabus?" (if the course keeps its syllabus in an external tool such as Simple Syllabus, you get the link to open instead) |
|
|
74
75
|
| People | "Who are the TAs for ECE 264?" · "Who is in my project group?" |
|
|
75
76
|
| Discussions | "Summarize the latest posts in the final project thread" |
|
|
76
77
|
| Lecture videos | "What did the professor say about pinch-off in Tuesday's recording?" (Kaltura, including BoilerCast LTI links, and YouTube) |
|
|
@@ -105,7 +106,7 @@ All optional, set in your AI app's MCP `env` config or your shell. The full list
|
|
|
105
106
|
|
|
106
107
|
## When it asks you to sign in
|
|
107
108
|
|
|
108
|
-
There's no separate login — asking a question signs you in, and it stays signed in on its own most days. When your school wants two-factor again, the number to approve shows up right in the answer; approve it on your phone and
|
|
109
|
+
There's no separate login — asking a question signs you in, and it stays signed in on its own most days. When your school wants two-factor again, the number to approve shows up right in the answer; approve it on your phone and the assistant keeps checking on its own — each check waits up to 45 seconds and the original question completes as soon as the sign-in does, without you having to say you've approved it. If your AI client shows tool progress messages, the number appears while the question is still running instead. If it ever gets stuck, run this in a terminal:
|
|
109
110
|
|
|
110
111
|
```bash
|
|
111
112
|
npx -y brightspace-mcp-server@latest auth
|
|
@@ -21,18 +21,13 @@ import { devActivity } from "../utils/dev-activity.js";
|
|
|
21
21
|
const AUTH_TIMEOUT_MS = 8 * 60 * 1000;
|
|
22
22
|
const KILL_GRACE_MS = 5000;
|
|
23
23
|
/**
|
|
24
|
-
* How long a caller
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
const JOIN_GRACE_MS = 5000;
|
|
32
|
-
/**
|
|
33
|
-
* How long a caller that can be told the challenge mid-call (see run()'s
|
|
34
|
-
* onChallenge) keeps waiting for the background sign-in after the challenge
|
|
35
|
-
* appears: long enough to find a phone and approve.
|
|
24
|
+
* How long a caller keeps waiting for the background sign-in once the
|
|
25
|
+
* challenge is known: long enough to find a phone and approve. Applies both
|
|
26
|
+
* to a caller told the challenge mid-call (see run()'s onChallenge) and to a
|
|
27
|
+
* retry that joins the background child after an early answer. The retry
|
|
28
|
+
* used to wait only 5 s, so on clients that send no progress token (Claude
|
|
29
|
+
* Desktop) every retry re-answered "enter the number" and the model asked
|
|
30
|
+
* the user to confirm by hand instead of the server polling for the approval.
|
|
36
31
|
*/
|
|
37
32
|
const MFA_POLL_MS = 45000;
|
|
38
33
|
/**
|
|
@@ -182,18 +177,20 @@ export class AuthRunner {
|
|
|
182
177
|
* Authenticate, joining the login this process already started if there is
|
|
183
178
|
* one. Returns true on success and throws a useful error on failure.
|
|
184
179
|
*
|
|
185
|
-
* Without onChallenge,
|
|
186
|
-
* mfaPending error
|
|
187
|
-
*
|
|
188
|
-
* up to MFA_POLL_MS,
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
180
|
+
* Without onChallenge, a NEW sign-in answers the caller at once when an MFA
|
|
181
|
+
* challenge appears (an mfaPending error carrying the number to enter: the
|
|
182
|
+
* user cannot see it any other way), and the next call joins the background
|
|
183
|
+
* sign-in and polls it for up to MFA_POLL_MS, never past CALL_BUDGET_MS from
|
|
184
|
+
* that call. With onChallenge, the caller is handed the challenge mid-call
|
|
185
|
+
* and the first call itself keeps waiting the same way. Either way an
|
|
186
|
+
* approval within the window completes the caller's original request; past
|
|
187
|
+
* it, the caller gets the same mfaPending answer again and is expected to
|
|
188
|
+
* call once more.
|
|
192
189
|
*/
|
|
193
190
|
async run(onChallenge) {
|
|
194
191
|
const startedAt = Date.now();
|
|
195
192
|
try {
|
|
196
|
-
return await this.runOnce();
|
|
193
|
+
return await this.runOnce(startedAt);
|
|
197
194
|
}
|
|
198
195
|
catch (error) {
|
|
199
196
|
const childDone = this.childDone;
|
|
@@ -209,7 +206,7 @@ export class AuthRunner {
|
|
|
209
206
|
return this.awaitBackgroundChild(childDone, windowMs);
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
|
-
runOnce() {
|
|
209
|
+
runOnce(startedAt) {
|
|
213
210
|
if (this.inFlight) {
|
|
214
211
|
log("DEBUG", "Joining the authentication already in flight");
|
|
215
212
|
return this.inFlight;
|
|
@@ -220,7 +217,7 @@ export class AuthRunner {
|
|
|
220
217
|
// cross-process lock and return "busy".
|
|
221
218
|
if (this.childDone) {
|
|
222
219
|
log("DEBUG", "Joining the background sign-in still running from an earlier call");
|
|
223
|
-
return this.joinBackgroundChild(this.childDone);
|
|
220
|
+
return this.joinBackgroundChild(this.childDone, startedAt);
|
|
224
221
|
}
|
|
225
222
|
// The latch is released by the flow that owns it, as it settles, so a
|
|
226
223
|
// failed login is never replayed: the tool call after a declined MFA
|
|
@@ -237,23 +234,23 @@ export class AuthRunner {
|
|
|
237
234
|
/**
|
|
238
235
|
* Join a background child from an earlier early-answered call instead of
|
|
239
236
|
* spawning a new one. A joiner must not simply await childDone: that
|
|
240
|
-
* blocks for whatever is left of the 5-minute approval window,
|
|
241
|
-
*
|
|
242
|
-
* after reading "call this tool again" and well before actually approving.
|
|
237
|
+
* blocks for whatever is left of the 5-minute approval window, past the
|
|
238
|
+
* client's request timeout.
|
|
243
239
|
*
|
|
244
240
|
* If no challenge has been reported yet (the child hasn't reached MFA),
|
|
245
241
|
* wait for one — or for the child to finish on its own. Once a challenge
|
|
246
|
-
* is known,
|
|
247
|
-
*
|
|
248
|
-
*
|
|
242
|
+
* is known, poll the child for the rest of this call's budget: the user
|
|
243
|
+
* read the number from the previous answer and is approving right now, so
|
|
244
|
+
* this is the call that should carry the result. A still-unapproved
|
|
245
|
+
* sign-in re-answers with the latest challenge and the caller tries again.
|
|
249
246
|
*/
|
|
250
|
-
async joinBackgroundChild(childDone) {
|
|
247
|
+
async joinBackgroundChild(childDone, startedAt) {
|
|
251
248
|
if (!this.pendingChallenge && this.challengeSignal) {
|
|
252
249
|
await Promise.race([childDone.catch(() => { }), this.challengeSignal]);
|
|
253
250
|
}
|
|
254
251
|
if (!this.pendingChallenge)
|
|
255
252
|
return childDone;
|
|
256
|
-
return this.awaitBackgroundChild(childDone,
|
|
253
|
+
return this.awaitBackgroundChild(childDone, Math.max(0, pollWindowMs(Date.now() - startedAt)));
|
|
257
254
|
}
|
|
258
255
|
/**
|
|
259
256
|
* Race a background child already past its MFA challenge against a wait
|
|
@@ -9,7 +9,58 @@ import { GetMyGradesSchema, } from "./schemas.js";
|
|
|
9
9
|
import { toolResponse, sanitizeError, errorResponse, isAuthUnavailable, authPendingNotice, } 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
|
+
import { gradebookUrl, quizUrl } from "../utils/deep-links.js";
|
|
13
|
+
/**
|
|
14
|
+
* The quiz a grade row is scored from can hold feedback the gradebook does
|
|
15
|
+
* not, and the student API cannot read it: quiz attempts are refused to
|
|
16
|
+
* students, and some quizzes only show feedback inside a restricted viewer.
|
|
17
|
+
*/
|
|
18
|
+
const QUIZ_FEEDBACK_NOTE = "The gradebook has no comment for this item, but it is scored from a quiz, and quiz " +
|
|
19
|
+
"feedback lives on the quiz's own submissions page, which Brightspace does not expose " +
|
|
20
|
+
"to the student API. Feedback may exist there, sometimes viewable only in a restricted " +
|
|
21
|
+
"browser such as Respondus LockDown Browser. Open feedbackUrl to check; do not report " +
|
|
22
|
+
"this item as having no feedback.";
|
|
23
|
+
/**
|
|
24
|
+
* The course's quizzes, or none when the route refuses or fails. A missing
|
|
25
|
+
* quiz list only costs the feedback links, never the grades themselves.
|
|
26
|
+
*/
|
|
27
|
+
async function fetchQuizzes(apiClient, courseId) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await apiClient.get(apiClient.le(courseId, "/quizzes/"), { ttl: DEFAULT_CACHE_TTLS.assignments });
|
|
30
|
+
return Array.isArray(raw) ? raw : raw?.Objects ?? [];
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
log("DEBUG", `get_my_grades: quiz list unavailable for course ${courseId}`, error);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Clean grade rows. Rows scored from a quiz also carry feedbackUrl, and a
|
|
39
|
+
* feedbackNote when the gradebook comment is empty; other rows are unchanged.
|
|
40
|
+
*/
|
|
41
|
+
function toGradeItems(gradeValues, quizzes, baseUrl, courseId) {
|
|
42
|
+
const quizByGradeItem = new Map(quizzes
|
|
43
|
+
.filter((q) => q.GradeItemId != null)
|
|
44
|
+
.map((q) => [String(q.GradeItemId), q.QuizId]));
|
|
45
|
+
return gradeValues.map((gv) => {
|
|
46
|
+
const comments = gv.Comments?.Text || null;
|
|
47
|
+
const quizId = quizByGradeItem.get(String(gv.GradeObjectIdentifier));
|
|
48
|
+
return {
|
|
49
|
+
name: gv.GradeObjectName,
|
|
50
|
+
displayGrade: gv.DisplayedGrade,
|
|
51
|
+
pointsNumerator: gv.PointsNumerator,
|
|
52
|
+
pointsDenominator: gv.PointsDenominator,
|
|
53
|
+
weightedNumerator: gv.WeightedNumerator,
|
|
54
|
+
weightedDenominator: gv.WeightedDenominator,
|
|
55
|
+
comments,
|
|
56
|
+
lastModified: gv.LastModified,
|
|
57
|
+
...(quizId === undefined ? {} : {
|
|
58
|
+
feedbackUrl: quizUrl(baseUrl, courseId, quizId),
|
|
59
|
+
...(comments ? {} : { feedbackNote: QUIZ_FEEDBACK_NOTE }),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
13
64
|
/**
|
|
14
65
|
* Register get_my_grades tool
|
|
15
66
|
*/
|
|
@@ -26,6 +77,7 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
26
77
|
// Single course case
|
|
27
78
|
if (courseId) {
|
|
28
79
|
const path = apiClient.le(courseId, "/grades/values/myGradeValues/");
|
|
80
|
+
const quizzesPromise = fetchQuizzes(apiClient, courseId);
|
|
29
81
|
let gradeValues;
|
|
30
82
|
try {
|
|
31
83
|
gradeValues = await apiClient.get(path, {
|
|
@@ -65,16 +117,7 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
65
117
|
throw error;
|
|
66
118
|
}
|
|
67
119
|
// Map to clean objects
|
|
68
|
-
const grades = gradeValues.
|
|
69
|
-
name: gv.GradeObjectName,
|
|
70
|
-
displayGrade: gv.DisplayedGrade,
|
|
71
|
-
pointsNumerator: gv.PointsNumerator,
|
|
72
|
-
pointsDenominator: gv.PointsDenominator,
|
|
73
|
-
weightedNumerator: gv.WeightedNumerator,
|
|
74
|
-
weightedDenominator: gv.WeightedDenominator,
|
|
75
|
-
comments: gv.Comments?.Text || null,
|
|
76
|
-
lastModified: gv.LastModified,
|
|
77
|
-
}));
|
|
120
|
+
const grades = toGradeItems(gradeValues, await quizzesPromise, config.baseUrl, courseId);
|
|
78
121
|
log("INFO", `get_my_grades: Retrieved ${grades.length} grade items for course ${courseId}`);
|
|
79
122
|
return toolResponse({ courseId, grades });
|
|
80
123
|
}
|
|
@@ -102,19 +145,11 @@ export function registerGetMyGrades(server, apiClient, config) {
|
|
|
102
145
|
const gradePromises = filteredEnrollments.map(async (item) => {
|
|
103
146
|
try {
|
|
104
147
|
const path = apiClient.le(item.OrgUnit.Id, "/grades/values/myGradeValues/");
|
|
105
|
-
const gradeValues = await
|
|
106
|
-
ttl: DEFAULT_CACHE_TTLS.grades,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
displayGrade: gv.DisplayedGrade,
|
|
111
|
-
pointsNumerator: gv.PointsNumerator,
|
|
112
|
-
pointsDenominator: gv.PointsDenominator,
|
|
113
|
-
weightedNumerator: gv.WeightedNumerator,
|
|
114
|
-
weightedDenominator: gv.WeightedDenominator,
|
|
115
|
-
comments: gv.Comments?.Text || null,
|
|
116
|
-
lastModified: gv.LastModified,
|
|
117
|
-
}));
|
|
148
|
+
const [gradeValues, quizzes] = await Promise.all([
|
|
149
|
+
apiClient.get(path, { ttl: DEFAULT_CACHE_TTLS.grades }),
|
|
150
|
+
fetchQuizzes(apiClient, item.OrgUnit.Id),
|
|
151
|
+
]);
|
|
152
|
+
const grades = toGradeItems(gradeValues, quizzes, config.baseUrl, item.OrgUnit.Id);
|
|
118
153
|
return {
|
|
119
154
|
restricted: false,
|
|
120
155
|
courseId: item.OrgUnit.Id,
|
|
@@ -13,13 +13,61 @@ import { extractPdfText } from "../utils/pdf-extractor.js";
|
|
|
13
13
|
import { log } from "../utils/logger.js";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import fs from "node:fs/promises";
|
|
16
|
+
const SYLLABUS_PATTERN = /syllab/i;
|
|
17
|
+
const LTI_LAUNCH_PATTERN = /[?&]type=lti\b|\/d2l\/le\/lti\//i;
|
|
18
|
+
const EXTERNAL_SYLLABUS_NOTE = "This course links its syllabus through an external LTI tool. The server lists these sources but cannot launch LTI tools, so their contents are not included; open the link in Brightspace to read it.";
|
|
19
|
+
/** Launch URL without its query string, which can carry launch parameters. */
|
|
20
|
+
function stripQuery(url) {
|
|
21
|
+
return url.split(/[?#]/)[0];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Pick the syllabus-related LTI sources out of the course's LTI links and
|
|
25
|
+
* content tree. Pure: the caller fetches, this only decides.
|
|
26
|
+
*/
|
|
27
|
+
function findExternalSyllabusSources(ltiLinks, toc) {
|
|
28
|
+
const sources = ltiLinks
|
|
29
|
+
.filter((l) => l.IsVisible !== false && (SYLLABUS_PATTERN.test(l.Title) || SYLLABUS_PATTERN.test(l.Url)))
|
|
30
|
+
.map((l) => ({ title: l.Title, location: "lti-link", url: stripQuery(l.Url) }));
|
|
31
|
+
const walk = (modules) => {
|
|
32
|
+
for (const m of modules) {
|
|
33
|
+
for (const t of m.Topics ?? []) {
|
|
34
|
+
if (t.Url && LTI_LAUNCH_PATTERN.test(t.Url) && SYLLABUS_PATTERN.test(t.Title)) {
|
|
35
|
+
sources.push({ title: t.Title, location: "content", url: t.Url });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
walk(m.Modules ?? []);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
walk(toc);
|
|
42
|
+
return sources;
|
|
43
|
+
}
|
|
44
|
+
/** Fetch a lookup that students may not be permitted to make; failure means "none". */
|
|
45
|
+
async function fetchOrEmpty(fetch, fallback, what) {
|
|
46
|
+
try {
|
|
47
|
+
return await fetch();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
log("DEBUG", `get_syllabus: could not read ${what}`, error);
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function discoverExternalSyllabusSources(apiClient, courseId) {
|
|
55
|
+
const [ltiLinks, toc] = await Promise.all([
|
|
56
|
+
fetchOrEmpty(() => apiClient.get(apiClient.leGlobal(`/lti/link/${courseId}/`), { ttl: DEFAULT_CACHE_TTLS.courseContent }), [], "LTI links"),
|
|
57
|
+
fetchOrEmpty(() => apiClient.get(apiClient.le(courseId, "/content/toc"), { ttl: DEFAULT_CACHE_TTLS.courseContent }), {}, "content table of contents"),
|
|
58
|
+
]);
|
|
59
|
+
const sources = findExternalSyllabusSources(ltiLinks ?? [], toc?.Modules ?? []);
|
|
60
|
+
return sources.length > 0
|
|
61
|
+
? { externalSyllabusSources: sources, externalSyllabusNote: EXTERNAL_SYLLABUS_NOTE }
|
|
62
|
+
: {};
|
|
63
|
+
}
|
|
16
64
|
/**
|
|
17
65
|
* Register get_syllabus tool
|
|
18
66
|
*/
|
|
19
67
|
export function registerGetSyllabus(server, apiClient) {
|
|
20
68
|
server.registerTool("get_syllabus", {
|
|
21
69
|
title: "Get Course Syllabus",
|
|
22
|
-
description: "Fetch the syllabus/overview text and optional attachment for a course. Returns the course overview description as markdown. If downloadPath is provided, also downloads the syllabus attachment (e.g. PDF). IMPORTANT: You MUST ask the user where they want to save the file before calling this tool with a downloadPath.",
|
|
70
|
+
description: "Fetch the syllabus/overview text and optional attachment for a course. Returns the course overview description as markdown. When the syllabus lives in an external LTI tool (e.g. Simple Syllabus), lists those sources in externalSyllabusSources; their contents cannot be read. If downloadPath is provided, also downloads the syllabus attachment (e.g. PDF). IMPORTANT: You MUST ask the user where they want to save the file before calling this tool with a downloadPath.",
|
|
23
71
|
inputSchema: GetSyllabusSchema,
|
|
24
72
|
}, async (args) => {
|
|
25
73
|
try {
|
|
@@ -55,6 +103,7 @@ export function registerGetSyllabus(server, apiClient) {
|
|
|
55
103
|
description: null,
|
|
56
104
|
hasAttachment: false,
|
|
57
105
|
message: "No syllabus/overview found for this course.",
|
|
106
|
+
...(await discoverExternalSyllabusSources(apiClient, courseId)),
|
|
58
107
|
});
|
|
59
108
|
}
|
|
60
109
|
throw error;
|
|
@@ -152,6 +201,7 @@ export function registerGetSyllabus(server, apiClient) {
|
|
|
152
201
|
if (download) {
|
|
153
202
|
result.download = download;
|
|
154
203
|
}
|
|
204
|
+
Object.assign(result, await discoverExternalSyllabusSources(apiClient, courseId));
|
|
155
205
|
return toolResponse(result);
|
|
156
206
|
}
|
|
157
207
|
catch (error) {
|
|
@@ -98,8 +98,10 @@ const AUTH_FAILURE_GUIDANCE = {
|
|
|
98
98
|
`Run \`${AUTH_COMMAND}\` in a terminal (from your home folder) to complete it with the number visible.`,
|
|
99
99
|
failed: `The sign-in did not complete. Run \`${AUTH_COMMAND}\` in a terminal (from your home folder) to see why, ` +
|
|
100
100
|
"or `brightspace-setup` if your saved school or username is wrong.",
|
|
101
|
-
mfaPending: "Approve the sign-in request on your phone (Microsoft Authenticator or Duo)
|
|
102
|
-
"then call this tool again
|
|
101
|
+
mfaPending: "Approve the sign-in request on your phone (Microsoft Authenticator or Duo). " +
|
|
102
|
+
"Tell the user that, then call this tool again right away without waiting for them to " +
|
|
103
|
+
"confirm: each call waits up to 45 seconds for the approval and returns the result as soon " +
|
|
104
|
+
"as the sign-in completes. Keep calling until it succeeds or reports a different error.",
|
|
103
105
|
};
|
|
104
106
|
/**
|
|
105
107
|
* mfaPending is the one kind whose guidance is partly dynamic: `numberMatch`
|
|
@@ -110,8 +112,10 @@ const AUTH_FAILURE_GUIDANCE = {
|
|
|
110
112
|
*/
|
|
111
113
|
function authFailureMessage(error) {
|
|
112
114
|
if (error.kind === "mfaPending" && error.numberMatch) {
|
|
113
|
-
return `Open Microsoft Authenticator and enter ${error.numberMatch} within 5 minutes
|
|
114
|
-
"then call this tool again
|
|
115
|
+
return `Open Microsoft Authenticator and enter ${error.numberMatch} within 5 minutes. ` +
|
|
116
|
+
"Tell the user that number, then call this tool again right away without waiting for them " +
|
|
117
|
+
"to confirm: each call waits up to 45 seconds for the approval and returns the result as " +
|
|
118
|
+
"soon as the sign-in completes. Keep calling until it succeeds or reports a different error.";
|
|
115
119
|
}
|
|
116
120
|
return AUTH_FAILURE_GUIDANCE[error.kind];
|
|
117
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.16",
|
|
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",
|