brightspace-d2l-mcp 1.4.3 → 1.4.5
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/CHANGELOG.md +9 -0
- package/README.md +16 -0
- package/build/api/errors.js +35 -1
- package/build/api/index.js +1 -1
- package/build/tools/get-announcements.js +1 -1
- package/build/tools/get-assignments.js +1 -1
- package/build/tools/get-my-courses.js +5 -0
- package/build/tools/get-my-grades.js +1 -1
- package/build/tools/get-upcoming-due-dates.js +11 -2
- package/build/tools/tool-helpers.js +6 -2
- package/llms.txt +26 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.5
|
|
4
|
+
|
|
5
|
+
- Add README tools and prompts sync tests to release.test.ts
|
|
6
|
+
|
|
7
|
+
## 1.4.4
|
|
8
|
+
|
|
9
|
+
- Make a stale course id actionable: the error now says to call `get_my_courses` to list valid course IDs, and a course tool that finds no enrollments at all points at running `npx brightspace-d2l-mcp setup` again
|
|
10
|
+
|
|
3
11
|
## 1.4.3
|
|
4
12
|
|
|
5
13
|
- Add README section on switching from brightspace-mcp-server
|
|
14
|
+
- Add docs/troubleshooting.md for connection, browser and sign in failures, linked from the README and llms.txt
|
|
6
15
|
|
|
7
16
|
## 1.4.2
|
|
8
17
|
|
package/README.md
CHANGED
|
@@ -251,6 +251,8 @@ App ids: `claude-desktop`, `claude-code`, `cursor`, `vscode`, `windsurf`, `codex
|
|
|
251
251
|
|
|
252
252
|
## Troubleshooting
|
|
253
253
|
|
|
254
|
+
Something not working? **[docs/troubleshooting.md](docs/troubleshooting.md)** walks through an app that shows no Brightspace tools, a setup window that never opens, and a single sign on that keeps failing, with the exact message each one prints. It starts with `npx brightspace-d2l-mcp doctor`.
|
|
255
|
+
|
|
254
256
|
<details>
|
|
255
257
|
<summary><b>"No supported browser was found"</b></summary>
|
|
256
258
|
|
|
@@ -386,6 +388,20 @@ node build/index.js setup # connects your AI apps to this local build
|
|
|
386
388
|
Tools live in [`src/tools/`](src/tools), sign-in in [`src/auth/`](src/auth), and setup in [`src/cli/`](src/cli).
|
|
387
389
|
Issues and pull requests are welcome.
|
|
388
390
|
|
|
391
|
+
## More free student MCP servers
|
|
392
|
+
|
|
393
|
+
All free, open source and read only where they touch your accounts. Browse the full list at [brightspacemcp.com/more-mcps](https://brightspacemcp.com/more-mcps/).
|
|
394
|
+
|
|
395
|
+
| Server | What it does |
|
|
396
|
+
| --- | --- |
|
|
397
|
+
| [Canvas MCP](https://github.com/AnonymousOnyx22/canvas-lms-student-mcp) | Canvas LMS for AI, read only. Due dates, missing work, grades and announcements. |
|
|
398
|
+
| [Moodle MCP](https://github.com/AnonymousOnyx22/moodle-student-mcp) | Moodle for AI, read only. Due dates, overdue work, grades and announcements. |
|
|
399
|
+
| [Google Classroom MCP](https://github.com/AnonymousOnyx22/google-classroom-student-mcp) | Google Classroom for AI, read only. Todos, due dates, missing work and grades. |
|
|
400
|
+
| [GPA Calculator MCP](https://github.com/AnonymousOnyx22/gpa-calculator-mcp) | GPA, grade conversion and the grade you need on the final, for AI. Works offline, no account. |
|
|
401
|
+
| [Citations MCP](https://github.com/AnonymousOnyx22/student-citations-mcp) | APA, MLA and Chicago citations from a DOI, an ISBN or typed details. No account. |
|
|
402
|
+
| [Study Planner MCP](https://github.com/AnonymousOnyx22/study-planner-mcp) | Exam study schedules, spaced repetition dates, pomodoro plans and assignment milestones. Works offline. |
|
|
403
|
+
| [Paper Finder MCP](https://github.com/AnonymousOnyx22/paper-finder-student-mcp) | Search scholarly papers, find open access copies and related work through OpenAlex. No key needed. |
|
|
404
|
+
|
|
389
405
|
## License
|
|
390
406
|
|
|
391
407
|
[MIT](LICENSE) © AnonymousOnyx22
|
package/build/api/errors.js
CHANGED
|
@@ -12,13 +12,28 @@ function withSessionHint(status, message) {
|
|
|
12
12
|
const detail = message.trim();
|
|
13
13
|
return detail ? `${detail} ${SESSION_EXPIRED_HINT}` : SESSION_EXPIRED_HINT;
|
|
14
14
|
}
|
|
15
|
+
// A stale course id must never reach the person as a bare 404. Every 404
|
|
16
|
+
// message names the tool that lists the valid ids, so the next step is obvious
|
|
17
|
+
// wherever the text shows up: a tool result mid-chat, a log line, or the CLI.
|
|
18
|
+
export const COURSE_NOT_FOUND_HINT = `Call \`get_my_courses\` to list valid course IDs, then retry.`;
|
|
19
|
+
// An empty enrollment list gives the caller nothing to work with, so the
|
|
20
|
+
// message points at the command that checks the sign in again.
|
|
21
|
+
export const NO_COURSES_HINT = `No courses came back from Brightspace. Run \`${SETUP_COMMAND}\` again to check your sign in, then retry.`;
|
|
22
|
+
// Appends the get_my_courses hint to a 404. A caller that already named the
|
|
23
|
+
// tool keeps its own wording, so the hint is never repeated in one message.
|
|
24
|
+
function withCourseHint(status, message) {
|
|
25
|
+
if (status !== 404 || message.includes("get_my_courses"))
|
|
26
|
+
return message;
|
|
27
|
+
const detail = message.trim();
|
|
28
|
+
return detail ? `${detail} ${COURSE_NOT_FOUND_HINT}` : COURSE_NOT_FOUND_HINT;
|
|
29
|
+
}
|
|
15
30
|
// Base class for all HTTP API errors
|
|
16
31
|
export class ApiError extends AuthError {
|
|
17
32
|
status;
|
|
18
33
|
endpoint;
|
|
19
34
|
responseBody;
|
|
20
35
|
constructor(status, endpoint, message, responseBody, cause) {
|
|
21
|
-
super(`[PBMCP-2001] API error (${status}) at ${endpoint}: ${withSessionHint(status, message)}`, cause);
|
|
36
|
+
super(`[PBMCP-2001] API error (${status}) at ${endpoint}: ${withSessionHint(status, withCourseHint(status, message))}`, cause);
|
|
22
37
|
this.status = status;
|
|
23
38
|
this.endpoint = endpoint;
|
|
24
39
|
this.responseBody = responseBody;
|
|
@@ -58,4 +73,23 @@ export class TokenRefreshError extends NetworkError {
|
|
|
58
73
|
this.name = "TokenRefreshError";
|
|
59
74
|
}
|
|
60
75
|
}
|
|
76
|
+
// The caller named a course id that is not in their enrollments. Thrown from
|
|
77
|
+
// the enrollment check, before any course scoped request goes out, so the
|
|
78
|
+
// person sees this instead of a bare 404 from Brightspace.
|
|
79
|
+
export class CourseNotFoundError extends Error {
|
|
80
|
+
courseId;
|
|
81
|
+
constructor(courseId) {
|
|
82
|
+
super(`Course ${courseId} is not one of your Brightspace courses. ${COURSE_NOT_FOUND_HINT}`);
|
|
83
|
+
this.courseId = courseId;
|
|
84
|
+
this.name = "CourseNotFoundError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// The enrollment list came back empty, so there is nothing for any course tool
|
|
88
|
+
// to read. Setting up again checks the saved sign in and the school address.
|
|
89
|
+
export class NoCoursesError extends Error {
|
|
90
|
+
constructor() {
|
|
91
|
+
super(NO_COURSES_HINT);
|
|
92
|
+
this.name = "NoCoursesError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
61
95
|
//# sourceMappingURL=errors.js.map
|
package/build/api/index.js
CHANGED
|
@@ -7,6 +7,6 @@ export { discoverVersions } from "./version-discovery.js";
|
|
|
7
7
|
export { TTLCache } from "./cache.js";
|
|
8
8
|
export { TokenBucket } from "./rate-limiter.js";
|
|
9
9
|
// Errors
|
|
10
|
-
export { ApiError, RateLimitError, NetworkError } from "./errors.js";
|
|
10
|
+
export { ApiError, RateLimitError, NetworkError, CourseNotFoundError, NoCoursesError, COURSE_NOT_FOUND_HINT, NO_COURSES_HINT, } from "./errors.js";
|
|
11
11
|
export { DEFAULT_CACHE_TTLS } from "./types.js";
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -38,7 +38,7 @@ export function mapNewsItem(item) {
|
|
|
38
38
|
export function registerGetAnnouncements(server, apiClient, config) {
|
|
39
39
|
server.registerTool("get_announcements", {
|
|
40
40
|
title: "Get Announcements",
|
|
41
|
-
description: "Fetch recent announcements from your courses. Can filter to a specific course or get announcements across all courses. Use this when the user asks about announcements, news, updates from instructors, recent posts, or what professors said. Example: \"Did my professors post anything new?\"",
|
|
41
|
+
description: "Fetch recent announcements (news items) from your courses in D2L Brightspace. Can filter to a specific course or get announcements across all courses. Use this when the user asks about announcements, news, news items, updates from instructors, recent posts, or what professors said in Brightspace or D2L. Example: \"Did my professors post anything new?\"",
|
|
42
42
|
inputSchema: GetAnnouncementsSchema,
|
|
43
43
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
44
44
|
}, async (args) => {
|
|
@@ -279,7 +279,7 @@ function gradebookHeadsUp(raw, fetched, courseId, baseUrl) {
|
|
|
279
279
|
export function registerGetAssignments(server, apiClient, config) {
|
|
280
280
|
server.registerTool("get_assignments", {
|
|
281
281
|
title: "Get Assignments",
|
|
282
|
-
description: "Fetch assignments and quizzes for a specific course or all enrolled courses. Shows dropbox submissions and quizzes with due dates, status, and rubric info. Use this when the user asks about assignments, homework, what to submit, quizzes, or assignment details and rubrics. Example: \"What assignments do I have in Calculus this week?\"",
|
|
282
|
+
description: "Fetch assignments and quizzes for a specific course or all enrolled courses in D2L Brightspace. Shows dropbox (assignment) submissions and quizzes with due dates, status, and rubric info. Use this when the user asks about assignments, homework, dropboxes, dropbox folders, what to submit, quizzes, or assignment details and rubrics in Brightspace or D2L. Example: \"What assignments do I have in Calculus this week?\"",
|
|
283
283
|
inputSchema: GetAssignmentsSchema,
|
|
284
284
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
285
285
|
}, async (args) => {
|
|
@@ -2,6 +2,7 @@ import { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
|
2
2
|
import { fetchAllItems } from "../api/paginate.js";
|
|
3
3
|
import { GetMyCoursesSchema, } from "./schemas.js";
|
|
4
4
|
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
5
|
+
import { NoCoursesError } from "../api/errors.js";
|
|
5
6
|
import { log } from "../utils/logger.js";
|
|
6
7
|
import { applyCourseFilter } from "../utils/course-filter.js";
|
|
7
8
|
export function registerGetMyCourses(server, apiClient, config) {
|
|
@@ -32,6 +33,10 @@ export function registerGetMyCourses(server, apiClient, config) {
|
|
|
32
33
|
canAccess: item.Access.CanAccess,
|
|
33
34
|
lastAccessed: item.Access.LastAccessed,
|
|
34
35
|
})), { ...config.courseFilter, activeOnly });
|
|
36
|
+
// An empty list leaves the caller stuck, so say which command checks
|
|
37
|
+
// the sign in again instead of returning nothing to act on.
|
|
38
|
+
if (courses.length === 0)
|
|
39
|
+
throw new NoCoursesError();
|
|
35
40
|
log("INFO", `get_my_courses: Retrieved ${courses.length} courses`);
|
|
36
41
|
return toolResponse(courses);
|
|
37
42
|
}
|
|
@@ -6,7 +6,7 @@ import { applyCourseFilter } from "../utils/course-filter.js";
|
|
|
6
6
|
export function registerGetMyGrades(server, apiClient, config) {
|
|
7
7
|
server.registerTool("get_my_grades", {
|
|
8
8
|
title: "Get My Grades",
|
|
9
|
-
description: "Fetch your grade breakdown for a specific course or all enrolled courses. Shows grade items with points, percentages, and comments. Use this when the user asks about grades, scores, marks, GPA, academic performance, or how they're doing in a class. Example: \"What's my current grade in Calculus?\"",
|
|
9
|
+
description: "Fetch your grade breakdown for a specific course or all enrolled courses in D2L Brightspace. Shows grade items (grade columns) with points, percentages, and comments. Use this when the user asks about grades, grade items, grade item, scores, marks, GPA, academic performance, or how they're doing in a class in Brightspace or D2L. Example: \"What's my current grade in Calculus?\"",
|
|
10
10
|
inputSchema: GetMyGradesSchema,
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
}, async (args) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CACHE_TTLS } from "../api/index.js";
|
|
2
|
+
import { CourseNotFoundError, NoCoursesError } from "../api/errors.js";
|
|
3
|
+
import { fetchAllItems } from "../api/paginate.js";
|
|
2
4
|
import { GetUpcomingDueDatesSchema } from "./schemas.js";
|
|
3
5
|
import { toolResponse, sanitizeError } from "./tool-helpers.js";
|
|
4
6
|
import { log } from "../utils/logger.js";
|
|
@@ -10,9 +12,10 @@ function unwrapList(raw) {
|
|
|
10
12
|
}
|
|
11
13
|
export async function resolveCourses(apiClient, config, courseId) {
|
|
12
14
|
let items = [];
|
|
15
|
+
let enrollmentsLoaded = false;
|
|
13
16
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
items = await fetchAllItems(apiClient, apiClient.lp("/enrollments/myenrollments/?orgUnitTypeId=3&isActive=true"), { ttl: DEFAULT_CACHE_TTLS.enrollments });
|
|
18
|
+
enrollmentsLoaded = true;
|
|
16
19
|
}
|
|
17
20
|
catch (error) {
|
|
18
21
|
if (!courseId)
|
|
@@ -21,6 +24,10 @@ export async function resolveCourses(apiClient, config, courseId) {
|
|
|
21
24
|
}
|
|
22
25
|
if (courseId) {
|
|
23
26
|
const match = items.find((item) => item.OrgUnit.Id === courseId);
|
|
27
|
+
// Only claim the id is unknown when the list really loaded. After a failed
|
|
28
|
+
// lookup the course scoped request is still the better test.
|
|
29
|
+
if (enrollmentsLoaded && !match)
|
|
30
|
+
throw new CourseNotFoundError(courseId);
|
|
24
31
|
return [{ id: courseId, name: match?.OrgUnit.Name ?? null }];
|
|
25
32
|
}
|
|
26
33
|
const filtered = applyCourseFilter(items.map((item) => ({
|
|
@@ -30,6 +37,8 @@ export async function resolveCourses(apiClient, config, courseId) {
|
|
|
30
37
|
isActive: item.Access.IsActive,
|
|
31
38
|
canAccess: item.Access.CanAccess,
|
|
32
39
|
})), config.courseFilter);
|
|
40
|
+
if (enrollmentsLoaded && filtered.length === 0)
|
|
41
|
+
throw new NoCoursesError();
|
|
33
42
|
return filtered.map((course) => ({ id: course.id, name: course.name }));
|
|
34
43
|
}
|
|
35
44
|
async function fetchCourseDueItems(apiClient, baseUrl, course) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { ApiError, RateLimitError, NetworkError } from "../api/index.js";
|
|
3
|
-
import { TokenRefreshError } from "../api/errors.js";
|
|
3
|
+
import { TokenRefreshError, CourseNotFoundError, NoCoursesError, COURSE_NOT_FOUND_HINT, } from "../api/errors.js";
|
|
4
4
|
import { AuthRequiredError } from "../auth/authenticator.js";
|
|
5
5
|
import { log } from "../utils/logger.js";
|
|
6
6
|
import { SETUP_COMMAND } from "../utils/paths.js";
|
|
@@ -36,9 +36,13 @@ export function sanitizeError(error) {
|
|
|
36
36
|
const detail = error.detail && isSafeDetail(error.detail) ? ` (detected type: ${error.detail})` : "";
|
|
37
37
|
return errorResponse(`Could not save the file.${detail} ${DOWNLOAD_FAILURE_GUIDANCE[error.kind]}`);
|
|
38
38
|
}
|
|
39
|
+
// Messages are written by this package, never copied from a remote response.
|
|
40
|
+
if (error instanceof CourseNotFoundError || error instanceof NoCoursesError) {
|
|
41
|
+
return errorResponse(error.message);
|
|
42
|
+
}
|
|
39
43
|
if (error instanceof ApiError) {
|
|
40
44
|
if (error.status === 404) {
|
|
41
|
-
return errorResponse(
|
|
45
|
+
return errorResponse(`Resource not found. The course or item may not exist, or you may not have access. ${COURSE_NOT_FOUND_HINT}`);
|
|
42
46
|
}
|
|
43
47
|
if (error.status === 401) {
|
|
44
48
|
return errorResponse(`Your Brightspace session expired and signing in again did not finish. Run \`${SETUP_COMMAND}\` in a terminal, then try again.`);
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Brightspace D2L MCP Server
|
|
2
2
|
|
|
3
|
-
> MCP server that gives AI assistants and agents read access to D2L Brightspace. It returns due dates with real submission status, grades, assignments, rubrics, course content, announcements, discussions and class lists. It works at any school that uses D2L Brightspace, including schools with Microsoft, Duo, Okta or Shibboleth sign-in. It is read only and never stores a password. Current version: 1.4.
|
|
3
|
+
> MCP server that gives AI assistants and agents read access to D2L Brightspace. It returns due dates with real submission status, grades, assignments, rubrics, course content, announcements, discussions and class lists. It works at any school that uses D2L Brightspace, including schools with Microsoft, Duo, Okta or Shibboleth sign-in. It is read only and never stores a password. Current version: 1.4.5.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -49,6 +49,30 @@ The server also offers MCP prompts: what_should_i_do_now, plan_my_week, weekly_d
|
|
|
49
49
|
|
|
50
50
|
Submission status values are submitted, graded, not_submitted, draft, not_open_yet, closed, past_due, restricted and unknown. Items that are submitted or graded are done and should never be suggested as a to-do. Quizzes can show unknown because Brightspace does not show students their quiz attempts, so ask the user.
|
|
51
51
|
|
|
52
|
+
## Which tool to use (decision table, routing table)
|
|
53
|
+
|
|
54
|
+
Short routing: due today -> get_todo, target grade -> get_grade_outlook, catch up -> get_whats_new.
|
|
55
|
+
|
|
56
|
+
| If the user asks about | Use this tool |
|
|
57
|
+
|---|---|
|
|
58
|
+
| What is due today, or what should I do now | get_todo |
|
|
59
|
+
| What they need on the remaining work to hit a target grade | get_grade_outlook |
|
|
60
|
+
| Catching up on what is new across every course | get_whats_new |
|
|
61
|
+
| Upcoming deadlines with real submission status | get_upcoming_due_dates |
|
|
62
|
+
| Exams, tests, labs or classes | get_calendar_events |
|
|
63
|
+
| Current grades and instructor comments | get_my_grades |
|
|
64
|
+
| Assignments and quizzes with rubrics | get_assignments |
|
|
65
|
+
| Files attached to an assignment | get_assignment_files |
|
|
66
|
+
| Course modules, slides, files and links | get_course_content |
|
|
67
|
+
| Syllabus and course overview | get_syllabus |
|
|
68
|
+
| Recent announcements | get_announcements |
|
|
69
|
+
| Discussion forums and posts | get_discussions |
|
|
70
|
+
| Instructors and TAs with contact info | get_roster |
|
|
71
|
+
| Email addresses for a course | get_classlist_emails |
|
|
72
|
+
| Saving a course file or submission | download_file |
|
|
73
|
+
| Exporting unfinished deadlines to a calendar | export_due_dates_calendar |
|
|
74
|
+
| Enrolled courses | get_my_courses |
|
|
75
|
+
|
|
52
76
|
## Other commands
|
|
53
77
|
|
|
54
78
|
- npx brightspace-d2l-mcp login signs in again
|
|
@@ -71,5 +95,6 @@ The password is never seen or stored because the user types it into the school's
|
|
|
71
95
|
## Links
|
|
72
96
|
|
|
73
97
|
- Repository https://github.com/AnonymousOnyx22/brightspace-d2l-mcp
|
|
98
|
+
- Troubleshooting https://github.com/AnonymousOnyx22/brightspace-d2l-mcp/blob/main/docs/troubleshooting.md
|
|
74
99
|
- npm https://www.npmjs.com/package/brightspace-d2l-mcp
|
|
75
100
|
- Agent setup guides https://github.com/AnonymousOnyx22/brightspace-d2l-mcp/blob/main/docs/agents.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-d2l-mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"mcpName": "io.github.AnonymousOnyx22/brightspace-d2l-mcp",
|
|
5
5
|
"description": "Brightspace MCP server for D2L: connect Brightspace to Claude, ChatGPT, Cursor, Copilot and any AI agent. One-command setup at any school with normal browser sign-in (SSO, Duo, MFA). Due dates with real submission status, exam and calendar events, what's new catch-up, grades and target-grade calculator, calendar export, assignments, content, announcements.",
|
|
6
6
|
"type": "module",
|