brightspace-mcp-server 5.0.1 → 5.0.3
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
CHANGED
|
@@ -69,7 +69,7 @@ School-specific quirks: [docs/sign-in.md](docs/sign-in.md#per-school-notes).
|
|
|
69
69
|
|-------|-----|
|
|
70
70
|
| Grades | "Am I passing all my classes?" · "Compare my grades across courses" · "What did my professor say about Mini-Assignment 1?" (own assignment feedback is read from every route Brightspace documents for students) · "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) |
|
|
71
71
|
| Due dates | "What's due in the next 48 hours?" · "Build me a study schedule for the week" |
|
|
72
|
-
| Assignments and rubrics | "What does the lab 4 spec actually ask for?" · "Why did I lose points on the analysis criterion?" |
|
|
72
|
+
| Assignments and rubrics | "What does the lab 4 spec actually ask for?" · "Why did I lose points on the analysis criterion?" · "Did I turn in HW 3?" (if Brightspace won't give students their own submission list, the assignment's feedback page is checked instead; when neither shows a submission it's reported as unknown, never as missing) |
|
|
73
73
|
| Quizzes and exams | "Which quizzes close this week?" · "Is there a midterm in the gradebook that isn't on my assignments list?" (quizzes are collected from the quiz list, the course content, and the gradebook, so a closed quiz or one the list leaves out still shows up; each course says whether its quiz sources all answered, and if Brightspace refused one it's marked incomplete rather than passed off as everything; "complete" means every source answered, not that every quiz is guaranteed to be listed. A gradebook column linked to a quiz the list omits now shows up as a quiz with its dates and attempts instead of a bare grade-only row when the quiz can be read. Gradebook-only rows now also appear on tenants that send only the documented `GradeType` on grade columns; hidden grade columns, hidden assignments and inactive quizzes never produce one. Content quizzes beyond the first 30 per course are listed from the course content alone and the course is marked incomplete. Quiz rows may also say whether they are open, closed, or not yet open, and mark a quiz whose details Brightspace would not return; gradebook lookups for unlisted quizzes are capped at 20 per course, or 5 when the course's quiz tool can't be identified) |
|
|
74
74
|
| Announcements | "Did any professor post something important today?" · "Read the file attached to today's announcement" |
|
|
75
75
|
| 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 |
|
|
@@ -390,6 +390,23 @@ function quizCoverage(answered) {
|
|
|
390
390
|
export async function fetchCourseAssignments(apiClient, courseId, baseUrl) {
|
|
391
391
|
return (await fetchCourseInventory(apiClient, courseId, baseUrl)).assignments;
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* True when a student's own dropbox feedback page lists at least one
|
|
395
|
+
* submission. Brightspace renders each submission as a row of its
|
|
396
|
+
* submitted-files grid: the numeric submission ID in a <label>, then the
|
|
397
|
+
* submitted file links (span.dfl), then the date. This reads that structure,
|
|
398
|
+
* not the column titles, which are translated and are present even as headers
|
|
399
|
+
* of an otherwise empty grid. A page with "No submissions made for this
|
|
400
|
+
* assignment", a login page, or any other page has no such row.
|
|
401
|
+
*/
|
|
402
|
+
function feedbackPageListsSubmission(html) {
|
|
403
|
+
for (const row of html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)) {
|
|
404
|
+
const cells = row[1];
|
|
405
|
+
if (/^\s*<td\b[^>]*>\s*<label>\s*\d+\s*<\/label>\s*<\/td>/i.test(cells) && /class="dfl"/i.test(cells))
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
393
410
|
/** A course's assignments and quizzes, with whether its quiz list is complete. */
|
|
394
411
|
async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
395
412
|
const assignments = [];
|
|
@@ -456,6 +473,22 @@ async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
|
456
473
|
log("DEBUG", `Could not determine submission state for folder ${folder.Id}`, error);
|
|
457
474
|
}
|
|
458
475
|
}
|
|
476
|
+
// When the API refuses the list, the student's own feedback page may
|
|
477
|
+
// still show it. Only a listed submission counts: a page with none, or
|
|
478
|
+
// no page at all, leaves the state unknown rather than "not submitted".
|
|
479
|
+
let submissionSource = null;
|
|
480
|
+
if (!submissionsKnown && (folder.GroupTypeId === null || folder.GroupTypeId === undefined)) {
|
|
481
|
+
try {
|
|
482
|
+
const html = await apiClient.getPage(`/d2l/lms/dropbox/user/folder_user_view_feedback.d2l?db=${folder.Id}&grpid=0&ou=${courseId}`);
|
|
483
|
+
if (html !== null && feedbackPageListsSubmission(html))
|
|
484
|
+
submissionSource = "feedbackPage";
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
if (isAuthUnavailable(error))
|
|
488
|
+
throw error;
|
|
489
|
+
log("DEBUG", `Feedback-page fallback failed for folder ${folder.Id}`, error);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
459
492
|
// Fetch feedback independently of submissions
|
|
460
493
|
let feedback = null;
|
|
461
494
|
let feedbackStatus = "unavailable";
|
|
@@ -550,12 +583,18 @@ async function fetchCourseInventory(apiClient, courseId, baseUrl) {
|
|
|
550
583
|
// when nothing is known, but submissionStatus tells apart "nothing
|
|
551
584
|
// was submitted" (404, a known answer) from "the answer is denied"
|
|
552
585
|
// (any other error), so a caller never states real work as missing.
|
|
553
|
-
submissionStatus:
|
|
554
|
-
? "
|
|
555
|
-
:
|
|
556
|
-
? "
|
|
557
|
-
:
|
|
558
|
-
|
|
586
|
+
submissionStatus: submissionSource
|
|
587
|
+
? "submitted"
|
|
588
|
+
: !submissionsKnown
|
|
589
|
+
? "unknown"
|
|
590
|
+
: submissions.length > 0
|
|
591
|
+
? "submitted"
|
|
592
|
+
: "not_submitted",
|
|
593
|
+
...(submissionSource ? {
|
|
594
|
+
submissionSource,
|
|
595
|
+
submissionStatusNote: "Brightspace did not return this assignment's submission list, but its feedback page lists a " +
|
|
596
|
+
"submission, so it was submitted. File details are only shown on that page.",
|
|
597
|
+
} : submissionsKnown ? {} : {
|
|
559
598
|
submissionStatusNote: "Brightspace did not return this assignment's submission list, so whether it was submitted " +
|
|
560
599
|
"is unknown. Do not report it as missing or unsubmitted - tell the user to check Brightspace directly.",
|
|
561
600
|
}),
|
|
@@ -99,6 +99,10 @@ export function readLtiLaunchPage(html, origin) {
|
|
|
99
99
|
const consumerKey = inputs.find(input => input.get("name") === "oauth_consumer_key")?.get("value");
|
|
100
100
|
const candidates = [
|
|
101
101
|
...tagAttributes(html, "form").map(form => form.get("action")),
|
|
102
|
+
// Brightspace's own quickLink page renders the tool as a web component
|
|
103
|
+
// whose lti-launch-url is the tool launch (/d2l/le/lti/…/toolLaunch/…),
|
|
104
|
+
// not a form or an iframe.
|
|
105
|
+
...tagAttributes(html, "d2l-lti-launch").map(element => element.get("lti-launch-url")),
|
|
102
106
|
...tagAttributes(html, "iframe").map(frame => frame.get("src")),
|
|
103
107
|
...inputs.map(input => input.get("value")),
|
|
104
108
|
].filter((candidate) => Boolean(candidate));
|
|
@@ -62,6 +62,16 @@ export function detectVideoPlatform(url) {
|
|
|
62
62
|
return "kaltura";
|
|
63
63
|
return "unknown";
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Kaltura's hosted KAF front ends are named after the school's partner ID:
|
|
67
|
+
* `983291-6.kaf.kaltura.com` is partner 983291 (instance 6). An LTI 1.3
|
|
68
|
+
* launch's target_link_uri points there with only the entry ID in the path,
|
|
69
|
+
* so the host is the one place the partner ID appears.
|
|
70
|
+
*/
|
|
71
|
+
function kafHostPartnerId(hostname) {
|
|
72
|
+
const match = /^(\d+)(?:-\d+)?\.kaf\.kaltura\.com$/i.exec(hostname);
|
|
73
|
+
return match ? match[1] : null;
|
|
74
|
+
}
|
|
65
75
|
/** Kaltura embeds carry the entry and partner ID as query params or path segments; naming varies by embed generator. */
|
|
66
76
|
export function extractKalturaIds(url) {
|
|
67
77
|
let parsed;
|
|
@@ -85,7 +95,7 @@ export function extractKalturaIds(url) {
|
|
|
85
95
|
params.get("pid") ??
|
|
86
96
|
pathSegmentParam(pathname, "wid") ??
|
|
87
97
|
pathSegmentParam(pathname, "partner_id");
|
|
88
|
-
const partnerId = widParam ? widParam.replace(/^_/, "") :
|
|
98
|
+
const partnerId = widParam ? widParam.replace(/^_/, "") : kafHostPartnerId(parsed.hostname);
|
|
89
99
|
if (!entryId || !partnerId)
|
|
90
100
|
return null;
|
|
91
101
|
return { partnerId, entryId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
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",
|