brightspace-d2l-mcp 1.3.0 → 1.4.1
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 +16 -0
- package/README.md +27 -2
- package/build/api/client.js +5 -4
- package/build/api/errors.js +14 -1
- package/build/cli/update-check.js +76 -0
- package/build/index.js +5 -2
- package/build/tools/download-file.js +1 -1
- package/build/tools/export-due-dates-calendar.js +2 -1
- package/build/tools/get-announcements.js +1 -1
- package/build/tools/get-assignment-files.js +1 -1
- package/build/tools/get-assignments.js +1 -1
- package/build/tools/get-calendar-events.js +2 -1
- package/build/tools/get-classlist-emails.js +2 -1
- package/build/tools/get-course-content.js +1 -1
- package/build/tools/get-discussions.js +1 -1
- package/build/tools/get-grade-outlook.js +3 -2
- package/build/tools/get-my-courses.js +1 -1
- package/build/tools/get-my-grades.js +1 -1
- package/build/tools/get-roster.js +1 -1
- package/build/tools/get-syllabus.js +1 -1
- package/build/tools/get-todo.js +2 -1
- package/build/tools/get-upcoming-due-dates.js +2 -1
- package/build/tools/get-whats-new.js +2 -1
- package/build/tools/tool-helpers.js +2 -2
- package/llms.txt +2 -2
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.1
|
|
4
|
+
|
|
5
|
+
- Make expired-session errors actionable in src/api/errors.ts
|
|
6
|
+
- Sync llms.txt and glama.json to the 17 tools and 9 prompts
|
|
7
|
+
- Put a vs-competitors table on the README first screen
|
|
8
|
+
- Add an example prompt to every tool description in src/index.ts
|
|
9
|
+
- Rewrite the npm description to lead with 'Brightspace MCP server'
|
|
10
|
+
- Update the MCP SDK to 1.32.1, playwright-core to 1.64.1, zod to 4.6.7 and unpdf to 1.9.1
|
|
11
|
+
- Keep file-type on 21 and vitest on 4, because those releases need Node 22 and this server still runs on Node 20
|
|
12
|
+
- Add release tests that keep package.json, server.json and the registry entry on one version, and that keep the build and test gate before publishing
|
|
13
|
+
|
|
14
|
+
## 1.4.0
|
|
15
|
+
|
|
16
|
+
- Update check. Running a command like `npx brightspace-d2l-mcp setup` on an older version now says which version is out and asks "Update now? (Y/n)". Yes switches to the new version and carries on with the same command. It never asks when an AI app runs the server, in CI, with `--yes`, or when `BRIGHTSPACE_NO_UPDATE_CHECK` is set.
|
|
17
|
+
- Running `npx brightspace-d2l-mcp` with no command in a terminal now shows help instead of silently starting the server. AI apps are unaffected.
|
|
18
|
+
|
|
3
19
|
## 1.3.0
|
|
4
20
|
|
|
5
21
|
- Setup closes its window when it finishes on Windows, after a 10 second countdown. Press Enter to close it straight away, type k to keep it open, or pass `--keep-open`. Every command now exits as soon as it is done, so the terminal is never left hanging.
|
package/README.md
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-9fb4ff?style=flat-square&labelColor=1b2033" alt="MIT license"></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.producthunt.com/products/brightspace-d2l-mcp-server?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-brightspace-d2l-mcp-server"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1279179&theme=neutral" alt="Brightspace D2L MCP Server on Product Hunt" width="250" height="54"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
15
19
|
<h3 align="center">Connect D2L Brightspace to Claude, ChatGPT, Cursor and Copilot.<br>It knows what you already handed in, so it only shows what is actually left.</h3>
|
|
16
20
|
|
|
17
21
|
<p align="center">One command. Sign in the way you always do. No passwords stored.</p>
|
|
18
22
|
|
|
19
|
-
<p align="center"
|
|
23
|
+
<p align="center"><a href="https://brightspace-mcp.netlify.app"><b>brightspace-mcp.netlify.app</b></a></p>
|
|
20
24
|
|
|
21
25
|
<br>
|
|
22
26
|
|
|
@@ -35,11 +39,30 @@ Then ask your AI:
|
|
|
35
39
|
|
|
36
40
|
> **"What should I work on right now?"**
|
|
37
41
|
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="assets/demo.gif" alt="Asking an AI what to work on and getting a Brightspace to-do list that skips submitted work (sample data)" width="760">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
38
46
|
<p align="center">
|
|
39
47
|
<img src="assets/setup.svg" alt="The setup command finding the school, signing in, and connecting AI apps" width="760">
|
|
40
48
|
</p>
|
|
41
49
|
|
|
42
50
|
New to this? The **[step-by-step setup guide](docs/setup.md)** walks through everything with pictures of each step.
|
|
51
|
+
Using a specific app? See the guides for **[Claude](https://brightspace-mcp.netlify.app/claude/)** and **[ChatGPT](https://brightspace-mcp.netlify.app/chatgpt/)**, or **[how this compares](https://brightspace-mcp.netlify.app/compare/)** to other Brightspace MCP servers.
|
|
52
|
+
|
|
53
|
+
## How it compares
|
|
54
|
+
|
|
55
|
+
People usually weigh a few Brightspace MCP servers before picking one. Here are the three differences that come up most, with the [full comparison](https://brightspace-mcp.netlify.app/compare/) for the rest:
|
|
56
|
+
|
|
57
|
+
| Server | Submission status | Sign-in (SSO) | Target grade |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| **Brightspace MCP** (this repo, `brightspace-d2l-mcp`) | Every due date says whether you already handed the work in, and the to-do list skips what is done | Real browser window where you sign in yourself, so any SSO works: Microsoft, Duo, Okta, Shibboleth. No password stored | `get_grade_outlook` gives your current percentage, your best and worst possible final grade, and what you need on the remaining work to hit any target |
|
|
60
|
+
| [brightspace-mcp-server](https://www.npmjs.com/package/brightspace-mcp-server) by RohanMuppa | Shown in its assignments tool | Automated sign-in that saves your password in Keychain or Credential Manager, with presets for nine schools | No target-grade tool in its published list |
|
|
61
|
+
| [brightspace-mcp](https://www.npmjs.com/package/brightspace-mcp) by JhostinAleck | Lists the files you submitted | Configured automation: HTTP login, headless browser with a Microsoft preset, Duo push, or pasted cookies | `grade_audit` answers "what you need to pass" |
|
|
62
|
+
| [@n35da/brightspace-mcp](https://www.npmjs.com/package/@n35da/brightspace-mcp) | Shown on each dropbox folder | Real browser window where you sign in with your school's SSO | No target-grade tool in its published list |
|
|
63
|
+
| [@exhabition/brightspace-mcp](https://www.npmjs.com/package/@exhabition/brightspace-mcp) (Rust) | Has assignment and submission readers | Visible Chromium window, Microsoft SSO only | No target-grade tool in its published list |
|
|
64
|
+
|
|
65
|
+
"No target-grade tool in its published list" means we could not find one in each project's public README and tool list as of October 2026, not that it can never be added. Spot a mistake in the table? Open an issue and it will be fixed.
|
|
43
66
|
|
|
44
67
|
## What you can ask
|
|
45
68
|
|
|
@@ -117,7 +140,7 @@ If your school uses D2L Brightspace, yes. Setup opens a normal browser window so
|
|
|
117
140
|
|
|
118
141
|
| School | Status |
|
|
119
142
|
|---|---|
|
|
120
|
-
|
|
|
143
|
+
| One college | Tested by the author |
|
|
121
144
|
| Any other D2L school | Should work. Open an issue with yours and I will add it here |
|
|
122
145
|
|
|
123
146
|
## Works with your AI app or agent
|
|
@@ -156,6 +179,8 @@ https://github.com/AnonymousOnyx22/brightspace-d2l-mcp/blob/main/llms.txt
|
|
|
156
179
|
| `npx brightspace-d2l-mcp serve --http` | Run over HTTP for ChatGPT and HTTP-based agents |
|
|
157
180
|
| `npx brightspace-d2l-mcp config` | Print the MCP config JSON for any other app |
|
|
158
181
|
|
|
182
|
+
When a newer version is out, commands ask whether to update first. Set `BRIGHTSPACE_NO_UPDATE_CHECK=1` to turn that off.
|
|
183
|
+
|
|
159
184
|
<details>
|
|
160
185
|
<summary><b>Setup options</b></summary>
|
|
161
186
|
|
package/build/api/client.js
CHANGED
|
@@ -5,7 +5,6 @@ import { discoverVersions } from "./version-discovery.js";
|
|
|
5
5
|
import { ApiError, RateLimitError, NetworkError } from "./errors.js";
|
|
6
6
|
import { withRetry, isRetryableFailure, retryAfterMsFrom } from "./retry.js";
|
|
7
7
|
import { log } from "../utils/logger.js";
|
|
8
|
-
import { LOGIN_COMMAND } from "../utils/paths.js";
|
|
9
8
|
import { packageVersion } from "../utils/version.js";
|
|
10
9
|
function isExpiredSessionRedirect(body, baseUrl) {
|
|
11
10
|
const expiredTarget = (target) => {
|
|
@@ -184,7 +183,9 @@ export class D2LApiClient {
|
|
|
184
183
|
}
|
|
185
184
|
log("WARN", "Auto-reauthentication did not produce a valid token");
|
|
186
185
|
}
|
|
187
|
-
|
|
186
|
+
// The 401 wording and the fix both live in errors.ts, so every dead-session
|
|
187
|
+
// error carries the same sign-in command.
|
|
188
|
+
throw new ApiError(401, path, "Brightspace rejected the saved session.");
|
|
188
189
|
}
|
|
189
190
|
async makeRequest(path, token) {
|
|
190
191
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -226,7 +227,7 @@ export class D2LApiClient {
|
|
|
226
227
|
// marker still parses, so it can never be misread as a dead session.
|
|
227
228
|
if (isExpiredSessionRedirect(responseBody, this.baseUrl)) {
|
|
228
229
|
log("DEBUG", "Response carried the session-expired stub, treating it as a 401");
|
|
229
|
-
throw new ApiError(401, path,
|
|
230
|
+
throw new ApiError(401, path, "Brightspace answered with its session-expired login page.");
|
|
230
231
|
}
|
|
231
232
|
throw new ApiError(response.status, path, `Expected JSON from ${path} but the body did not parse`);
|
|
232
233
|
}
|
|
@@ -283,7 +284,7 @@ export class D2LApiClient {
|
|
|
283
284
|
const body = await response.text();
|
|
284
285
|
if (isExpiredSessionRedirect(body, this.baseUrl)) {
|
|
285
286
|
log("DEBUG", "File download answered with the session-expired stub, treating it as a 401");
|
|
286
|
-
throw new ApiError(401, path,
|
|
287
|
+
throw new ApiError(401, path, "Brightspace answered with its session-expired login page.");
|
|
287
288
|
}
|
|
288
289
|
// A legitimate HTML page: hand back an equivalent response with the
|
|
289
290
|
// body we already consumed.
|
package/build/api/errors.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { AuthError } from "../utils/errors.js";
|
|
2
|
+
import { SETUP_COMMAND } from "../utils/paths.js";
|
|
3
|
+
// A dead Brightspace session must never reach the person as a bare 401. Every
|
|
4
|
+
// 401 message carries the command that signs them in again, so the text stays
|
|
5
|
+
// fixable wherever it shows up: a tool result mid-chat, a log line, or the CLI.
|
|
6
|
+
export const SESSION_EXPIRED_HINT = `Your Brightspace session expired. Run \`${SETUP_COMMAND}\` again to sign in, then retry.`;
|
|
7
|
+
// Appends the sign-in command to a 401. A caller that already named the command
|
|
8
|
+
// keeps its own wording, so the hint is never repeated in one message.
|
|
9
|
+
function withSessionHint(status, message) {
|
|
10
|
+
if (status !== 401 || message.includes(SETUP_COMMAND))
|
|
11
|
+
return message;
|
|
12
|
+
const detail = message.trim();
|
|
13
|
+
return detail ? `${detail} ${SESSION_EXPIRED_HINT}` : SESSION_EXPIRED_HINT;
|
|
14
|
+
}
|
|
2
15
|
// Base class for all HTTP API errors
|
|
3
16
|
export class ApiError extends AuthError {
|
|
4
17
|
status;
|
|
5
18
|
endpoint;
|
|
6
19
|
responseBody;
|
|
7
20
|
constructor(status, endpoint, message, responseBody, cause) {
|
|
8
|
-
super(`[PBMCP-2001] API error (${status}) at ${endpoint}: ${message}`, cause);
|
|
21
|
+
super(`[PBMCP-2001] API error (${status}) at ${endpoint}: ${withSessionHint(status, message)}`, cause);
|
|
9
22
|
this.status = status;
|
|
10
23
|
this.endpoint = endpoint;
|
|
11
24
|
this.responseBody = responseBody;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { PACKAGE_NAME } from "../utils/paths.js";
|
|
4
|
+
import { packageVersion } from "../utils/version.js";
|
|
5
|
+
import { isNewerVersion } from "./doctor.js";
|
|
6
|
+
import * as ui from "./ui.js";
|
|
7
|
+
const CHECK_TIMEOUT_MS = 1500;
|
|
8
|
+
/** Set on the relaunched process so it never asks again. */
|
|
9
|
+
const RELAUNCHED_ENV = "BRIGHTSPACE_UPDATED";
|
|
10
|
+
/** Commands that never prompt: the MCP server itself, and output meant for scripts. */
|
|
11
|
+
const SKIP_COMMANDS = new Set(["serve", "config", "--version", "-v"]);
|
|
12
|
+
export function shouldCheckForUpdate(command, args, env = process.env, tty = process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
13
|
+
if (!tty || SKIP_COMMANDS.has(command))
|
|
14
|
+
return false;
|
|
15
|
+
if (args.includes("--yes") || args.includes("-y"))
|
|
16
|
+
return false;
|
|
17
|
+
return !env.CI && !env.BRIGHTSPACE_NO_UPDATE_CHECK && !env[RELAUNCHED_ENV];
|
|
18
|
+
}
|
|
19
|
+
async function latestVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
return null;
|
|
24
|
+
const { version } = (await response.json());
|
|
25
|
+
return typeof version === "string" ? version : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function quoteForCmd(arg) {
|
|
32
|
+
return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
33
|
+
}
|
|
34
|
+
function relaunch(version, command, args) {
|
|
35
|
+
const windows = process.platform === "win32";
|
|
36
|
+
const argv = ["-y", `${PACKAGE_NAME}@${version}`, command, ...args];
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const child = spawn("npx", windows ? argv.map(quoteForCmd) : argv, {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
shell: windows,
|
|
41
|
+
// Run outside the current folder, so a project that itself depends on this package can't shadow the new version.
|
|
42
|
+
cwd: homedir(),
|
|
43
|
+
env: { ...process.env, [RELAUNCHED_ENV]: "1" },
|
|
44
|
+
});
|
|
45
|
+
child.on("error", () => resolve(-1));
|
|
46
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Before an interactive command, offers to switch to a newer release.
|
|
51
|
+
* Returns the exit code of the updated run, or null to carry on with this version.
|
|
52
|
+
*/
|
|
53
|
+
export async function offerUpdate(command, args) {
|
|
54
|
+
if (!shouldCheckForUpdate(command, args))
|
|
55
|
+
return null;
|
|
56
|
+
const current = packageVersion();
|
|
57
|
+
const latest = await latestVersion();
|
|
58
|
+
if (!latest || !isNewerVersion(latest, current))
|
|
59
|
+
return null;
|
|
60
|
+
console.log(`\n ${ui.yellow("↑")} ${ui.bold(`brightspace-d2l-mcp v${latest}`)} is out ${ui.dim(`(you have v${current})`)}`);
|
|
61
|
+
if (!(await ui.confirm("Update now?"))) {
|
|
62
|
+
ui.info(ui.dim(`Skipped. Run npx -y ${PACKAGE_NAME}@latest ${command} any time to update.`));
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
ui.info(ui.dim(`Downloading v${latest}...`));
|
|
66
|
+
const code = await relaunch(latest, command, args);
|
|
67
|
+
if (code === -1) {
|
|
68
|
+
ui.warn(`Couldn't start the update. Carrying on with v${current}.`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!process.argv[1]?.includes("_npx")) {
|
|
72
|
+
ui.info(ui.dim(`To update your installed copy too, run: npm install -g ${PACKAGE_NAME}@latest`));
|
|
73
|
+
}
|
|
74
|
+
return code;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=update-check.js.map
|
package/build/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const CLI_COMMANDS = new Set(["setup", "login", "status", "doctor", "logout", "uninstall", "serve", "config", "help", "--help", "-h", "--version", "-v"]);
|
|
3
|
-
|
|
3
|
+
// AI apps launch the server with piped stdio. A person typing the bare command in a terminal gets help instead of a silent server.
|
|
4
|
+
const command = process.argv[2] ?? (process.stdin.isTTY ? "help" : undefined);
|
|
4
5
|
if (command && CLI_COMMANDS.has(command)) {
|
|
5
6
|
const { runCli } = await import("./cli/main.js");
|
|
6
|
-
const
|
|
7
|
+
const { offerUpdate } = await import("./cli/update-check.js");
|
|
8
|
+
const args = process.argv.slice(3);
|
|
9
|
+
const code = (await offerUpdate(command, args)) ?? (await runCli(command, args));
|
|
7
10
|
// Exit as soon as a command finishes. A browser or timer left behind would otherwise keep the terminal busy.
|
|
8
11
|
// `serve` only returns once its stdio server is running, so it keeps the process alive.
|
|
9
12
|
if (command === "serve")
|
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
export function registerDownloadFile(server, apiClient) {
|
|
9
9
|
server.registerTool("download_file", {
|
|
10
10
|
title: "Download File",
|
|
11
|
-
description: "Download a file from course content or assignment submissions to a local directory. Use this when the user wants to download, save, or get a file from Brightspace course content or dropbox submissions. IMPORTANT: You MUST ask the user where they want to save the file before calling this tool. Never guess or assume a download directory. After identifying the file to download, suggest a clean readable filename to the user (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and ask if they'd like to rename it. Pass their preferred name as customFilename, or omit it to keep the original.",
|
|
11
|
+
description: "Download a file from course content or assignment submissions to a local directory. Use this when the user wants to download, save, or get a file from Brightspace course content or dropbox submissions. Example: \"Save the week 4 problem set PDF to my homework folder.\" IMPORTANT: You MUST ask the user where they want to save the file before calling this tool. Never guess or assume a download directory. After identifying the file to download, suggest a clean readable filename to the user (e.g., 'Lecture 7 - Memory Management.pdf' instead of 'L07_CS251_2026SP_v2.pdf') and ask if they'd like to rename it. Pass their preferred name as customFilename, or omit it to keep the original.",
|
|
12
12
|
inputSchema: DownloadFileSchema,
|
|
13
13
|
}, async (args) => {
|
|
14
14
|
try {
|
|
@@ -13,7 +13,8 @@ export function registerExportDueDatesCalendar(server, apiClient, config) {
|
|
|
13
13
|
title: "Export Due Dates To Calendar",
|
|
14
14
|
description: "Builds an iCalendar (.ics) file of upcoming assignments and quizzes that opens in Google Calendar, Apple Calendar " +
|
|
15
15
|
"and Outlook. Each event has a link back to Brightspace and a one day reminder. By default only unfinished work is " +
|
|
16
|
-
"included. Pass downloadPath (an absolute folder) to save the file, otherwise the calendar text is returned."
|
|
16
|
+
"included. Pass downloadPath (an absolute folder) to save the file, otherwise the calendar text is returned. " +
|
|
17
|
+
"Example: \"Put my upcoming due dates in Google Calendar.\"",
|
|
17
18
|
inputSchema: ExportDueDatesCalendarSchema,
|
|
18
19
|
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
19
20
|
}, async (args) => {
|
|
@@ -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.",
|
|
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?\"",
|
|
42
42
|
inputSchema: GetAnnouncementsSchema,
|
|
43
43
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
44
44
|
}, async (args) => {
|
|
@@ -90,7 +90,7 @@ async function readAttachment(apiClient, courseId, folderId, attachment, extract
|
|
|
90
90
|
export function registerGetAssignmentFiles(server, apiClient, baseUrl) {
|
|
91
91
|
server.registerTool("get_assignment_files", {
|
|
92
92
|
title: "Get Assignment Files",
|
|
93
|
-
description: "Read the files an instructor attached to an assignment: the spec or instructions PDF, a starter workbook, a rubric document. Call it with just courseId to see which assignments have attachments, then with folderId and fileId to read one. Use this when the user asks what an assignment requires, what the instructions say, or to summarize a handout. Returns the text itself. Use download_file instead when the user wants the file saved to disk.",
|
|
93
|
+
description: "Read the files an instructor attached to an assignment: the spec or instructions PDF, a starter workbook, a rubric document. Call it with just courseId to see which assignments have attachments, then with folderId and fileId to read one. Use this when the user asks what an assignment requires, what the instructions say, or to summarize a handout. Returns the text itself. Use download_file instead when the user wants the file saved to disk. Example: \"What does the lab report assignment ask for?\"",
|
|
94
94
|
inputSchema: GetAssignmentFilesSchema,
|
|
95
95
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
96
96
|
}, 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.",
|
|
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?\"",
|
|
283
283
|
inputSchema: GetAssignmentsSchema,
|
|
284
284
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
285
285
|
}, async (args) => {
|
|
@@ -37,7 +37,8 @@ export function registerGetCalendarEvents(server, apiClient, config) {
|
|
|
37
37
|
title: "Get Calendar Events",
|
|
38
38
|
description: "Events from the Brightspace course calendar: exams, tests, labs, classes, office hours and other dated events " +
|
|
39
39
|
"that instructors post, plus due-date events. Use this when the user asks when an exam or test is, what is on their " +
|
|
40
|
-
"schedule, or what is happening on a day. For what work is left to hand in, use get_todo instead."
|
|
40
|
+
"schedule, or what is happening on a day. For what work is left to hand in, use get_todo instead. " +
|
|
41
|
+
"Example: \"When is my final exam?\"",
|
|
41
42
|
inputSchema: GetCalendarEventsSchema,
|
|
42
43
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
43
44
|
}, async (args) => {
|
|
@@ -9,7 +9,8 @@ export function registerGetClasslistEmails(server, apiClient) {
|
|
|
9
9
|
description: "Fetch all email addresses for everyone in a course: instructors, TAs, and students. " +
|
|
10
10
|
"Use this when the user wants a list of emails for a class, needs to email the whole class, " +
|
|
11
11
|
"or wants contact info for everyone enrolled. Returns emails only, with no names or roles. " +
|
|
12
|
-
"For names, roles or finding a specific instructor or TA, use get_roster instead."
|
|
12
|
+
"For names, roles or finding a specific instructor or TA, use get_roster instead. " +
|
|
13
|
+
"Example: \"Give me everyone's email in my Biology course.\"",
|
|
13
14
|
inputSchema: GetClasslistEmailsSchema,
|
|
14
15
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
15
16
|
}, async (args) => {
|
|
@@ -124,7 +124,7 @@ function countModules(tree) {
|
|
|
124
124
|
export function registerGetCourseContent(server, apiClient) {
|
|
125
125
|
server.registerTool("get_course_content", {
|
|
126
126
|
title: "Get Course Content",
|
|
127
|
-
description: "Fetch the content tree for a course showing modules, topics, files, and links. Use this when the user asks about course materials, lecture slides, uploaded files, content structure, or what's in a course module. Use moduleTitle to filter to a specific module (e.g. 'Labs', 'Staff', 'Homeworks') instead of fetching the entire tree. Use maxDepth to limit recursion depth for a table-of-contents view.",
|
|
127
|
+
description: "Fetch the content tree for a course showing modules, topics, files, and links. Use this when the user asks about course materials, lecture slides, uploaded files, content structure, or what's in a course module. Use moduleTitle to filter to a specific module (e.g. 'Labs', 'Staff', 'Homeworks') instead of fetching the entire tree. Use maxDepth to limit recursion depth for a table-of-contents view. Example: \"Where are the lecture slides for week 4?\"",
|
|
128
128
|
inputSchema: GetCourseContentSchema,
|
|
129
129
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
130
130
|
}, async (args) => {
|
|
@@ -6,7 +6,7 @@ import { log } from "../utils/logger.js";
|
|
|
6
6
|
export function registerGetDiscussions(server, apiClient) {
|
|
7
7
|
server.registerTool("get_discussions", {
|
|
8
8
|
title: "Get Discussions",
|
|
9
|
-
description: "Fetch discussion board content for a course including forums, topics, and posts. Use this when the user asks about discussion boards, forum posts, class discussions, or wants to see what's been posted. Provide just courseId to list all forums and their topics. Add forumId to get topics and posts for a specific forum. Add both forumId and topicId to get all posts in a specific discussion topic.",
|
|
9
|
+
description: "Fetch discussion board content for a course including forums, topics, and posts. Use this when the user asks about discussion boards, forum posts, class discussions, or wants to see what's been posted. Provide just courseId to list all forums and their topics. Add forumId to get topics and posts for a specific forum. Add both forumId and topicId to get all posts in a specific discussion topic. Example: \"What have people posted in this week's discussion board?\"",
|
|
10
10
|
inputSchema: GetDiscussionsSchema,
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
}, async (args) => {
|
|
@@ -7,8 +7,9 @@ export function registerGetGradeOutlook(server, apiClient) {
|
|
|
7
7
|
title: "Grade Outlook And Target Calculator",
|
|
8
8
|
description: "Works out where the user stands in one course and what they need on the remaining work to reach a target grade. " +
|
|
9
9
|
"Returns the current percentage, the best and worst possible final grade, and the average they must score on " +
|
|
10
|
-
"everything left to hit targetPercent. Use for \"
|
|
11
|
-
"
|
|
10
|
+
"everything left to hit targetPercent. Use for \"can I still pass?\" or \"am I on track?\". " +
|
|
11
|
+
"For the raw grade items, use get_my_grades instead. " +
|
|
12
|
+
"Example: \"What do I need on the final to get an 80?\"",
|
|
12
13
|
inputSchema: GetGradeOutlookSchema,
|
|
13
14
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
14
15
|
}, async (args) => {
|
|
@@ -7,7 +7,7 @@ import { applyCourseFilter } from "../utils/course-filter.js";
|
|
|
7
7
|
export function registerGetMyCourses(server, apiClient, config) {
|
|
8
8
|
server.registerTool("get_my_courses", {
|
|
9
9
|
title: "Get My Courses",
|
|
10
|
-
description: "Fetch your enrolled Brightspace courses with names, codes, and IDs. Use this when the user asks about their courses, enrolled classes, what they're taking this semester, or needs a course ID for other queries.",
|
|
10
|
+
description: "Fetch your enrolled Brightspace courses with names, codes, and IDs. Use this when the user asks about their courses, enrolled classes, what they're taking this semester, or needs a course ID for other queries. Example: \"What courses am I taking this term?\"",
|
|
11
11
|
inputSchema: GetMyCoursesSchema,
|
|
12
12
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
13
13
|
}, async (args) => {
|
|
@@ -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.",
|
|
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?\"",
|
|
10
10
|
inputSchema: GetMyGradesSchema,
|
|
11
11
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
12
12
|
}, async (args) => {
|
|
@@ -22,7 +22,7 @@ async function fetchClasslistUsers(apiClient, courseId, options) {
|
|
|
22
22
|
export function registerGetRoster(server, apiClient) {
|
|
23
23
|
server.registerTool("get_roster", {
|
|
24
24
|
title: "Get Course Roster",
|
|
25
|
-
description: "Fetch the roster for a course including instructors, TAs, and optionally students with their names, emails, and roles. Use this when the user asks about classmates, instructor contact info, TA emails, professor names, or who's in a class. By default returns only instructors and TAs for privacy. Use includeStudents to get full class list. For just a plain list of email addresses to copy or email everyone, use get_classlist_emails instead.",
|
|
25
|
+
description: "Fetch the roster for a course including instructors, TAs, and optionally students with their names, emails, and roles. Use this when the user asks about classmates, instructor contact info, TA emails, professor names, or who's in a class. By default returns only instructors and TAs for privacy. Use includeStudents to get full class list. For just a plain list of email addresses to copy or email everyone, use get_classlist_emails instead. Example: \"Who's the TA for my Chemistry class and what's their email?\"",
|
|
26
26
|
inputSchema: GetRosterSchema,
|
|
27
27
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
28
28
|
}, async (args) => {
|
|
@@ -11,7 +11,7 @@ import fs from "node:fs/promises";
|
|
|
11
11
|
export function registerGetSyllabus(server, apiClient) {
|
|
12
12
|
server.registerTool("get_syllabus", {
|
|
13
13
|
title: "Get Course Syllabus",
|
|
14
|
-
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.",
|
|
14
|
+
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). Example: \"What's the grading breakdown in my Physics course?\" IMPORTANT: You MUST ask the user where they want to save the file before calling this tool with a downloadPath.",
|
|
15
15
|
inputSchema: GetSyllabusSchema,
|
|
16
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
17
|
}, async (args) => {
|
package/build/tools/get-todo.js
CHANGED
|
@@ -43,7 +43,8 @@ export function registerGetTodo(server, apiClient, config) {
|
|
|
43
43
|
description: "Prioritized to-do list across all courses: unfinished work due soon, recently overdue work, " +
|
|
44
44
|
"work not open yet, items to double-check (quizzes), and work already submitted. " +
|
|
45
45
|
"Use this first when the user asks what to work on, what's left, or what they should do now. " +
|
|
46
|
-
"For a plain chronological list of deadlines or a custom date range, use get_upcoming_due_dates instead."
|
|
46
|
+
"For a plain chronological list of deadlines or a custom date range, use get_upcoming_due_dates instead. " +
|
|
47
|
+
"Example: \"What should I work on right now?\"",
|
|
47
48
|
inputSchema: GetTodoSchema,
|
|
48
49
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49
50
|
}, async (args) => {
|
|
@@ -122,7 +122,8 @@ export function registerGetUpcomingDueDates(server, apiClient, config) {
|
|
|
122
122
|
description: "Upcoming due dates across all courses, with whether each assignment is already submitted " +
|
|
123
123
|
"(submissionStatus: submitted, graded, draft, not_submitted, not_open_yet, closed, past_due, restricted, unknown). " +
|
|
124
124
|
"Dates are UTC ISO strings; convert to the user's local time. Use for deadlines, what's due and date ranges, " +
|
|
125
|
-
"in date order. For advice on what to do first, use get_todo instead."
|
|
125
|
+
"in date order. For advice on what to do first, use get_todo instead. " +
|
|
126
|
+
"Example: \"What's due next week?\"",
|
|
126
127
|
inputSchema: GetUpcomingDueDatesSchema,
|
|
127
128
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
128
129
|
}, async (args) => {
|
|
@@ -44,7 +44,8 @@ export function registerGetWhatsNew(server, apiClient, config) {
|
|
|
44
44
|
title: "What's New",
|
|
45
45
|
description: "One catch-up summary across all courses: new announcements, unread assignment feedback (newly marked work), " +
|
|
46
46
|
"unread discussion posts and quizzes not attempted yet. Use this when the user asks what's new, what they missed, " +
|
|
47
|
-
"whether anything was graded or posted, or for a daily or weekly catch-up."
|
|
47
|
+
"whether anything was graded or posted, or for a daily or weekly catch-up. " +
|
|
48
|
+
"Example: \"What did I miss in my courses this week?\"",
|
|
48
49
|
inputSchema: GetWhatsNewSchema,
|
|
49
50
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
50
51
|
}, async (args) => {
|
|
@@ -3,7 +3,7 @@ import { ApiError, RateLimitError, NetworkError } from "../api/index.js";
|
|
|
3
3
|
import { TokenRefreshError } from "../api/errors.js";
|
|
4
4
|
import { AuthRequiredError } from "../auth/authenticator.js";
|
|
5
5
|
import { log } from "../utils/logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SETUP_COMMAND } from "../utils/paths.js";
|
|
7
7
|
import { DownloadError, isSafeDetail, } from "../utils/download-errors.js";
|
|
8
8
|
export function toolResponse(data) {
|
|
9
9
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -41,7 +41,7 @@ export function sanitizeError(error) {
|
|
|
41
41
|
return errorResponse("Resource not found. The course or item may not exist, or you may not have access.");
|
|
42
42
|
}
|
|
43
43
|
if (error.status === 401) {
|
|
44
|
-
return errorResponse(`Your Brightspace session expired and signing in again did not finish. Run \`${
|
|
44
|
+
return errorResponse(`Your Brightspace session expired and signing in again did not finish. Run \`${SETUP_COMMAND}\` in a terminal, then try again.`);
|
|
45
45
|
}
|
|
46
46
|
if (error.status === 403) {
|
|
47
47
|
return errorResponse("Access denied. You may not have permission to access this resource.");
|
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.
|
|
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.1.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ For any other app run npx brightspace-d2l-mcp config to print the MCP config JSO
|
|
|
47
47
|
|
|
48
48
|
The server also offers MCP prompts: what_should_i_do_now, plan_my_week, weekly_digest, grade_target, exam_prep, summarize_assignment, put_deadlines_in_my_calendar, catch_me_up and when_are_my_exams.
|
|
49
49
|
|
|
50
|
-
Submission status values are submitted, graded, not_submitted, draft, not_open_yet, closed, past_due 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.
|
|
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
52
|
## Other commands
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-d2l-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"mcpName": "io.github.AnonymousOnyx22/brightspace-d2l-mcp",
|
|
5
|
-
"description": "MCP server for D2L
|
|
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",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"bin": {
|
|
@@ -28,26 +28,26 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build && npm test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
32
|
-
"file-type": "^21.3.
|
|
33
|
-
"playwright-core": "^1.
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
32
|
+
"file-type": "^21.3.4",
|
|
33
|
+
"playwright-core": "^1.64.1",
|
|
34
34
|
"sanitize-filename": "^1.6.3",
|
|
35
35
|
"turndown": "^7.2.2",
|
|
36
|
-
"unpdf": "^1.
|
|
37
|
-
"zod": "^4.
|
|
36
|
+
"unpdf": "^1.9.1",
|
|
37
|
+
"zod": "^4.6.7"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^22.
|
|
40
|
+
"@types/node": "^22.20.5",
|
|
41
41
|
"@types/turndown": "^5.0.6",
|
|
42
|
-
"typescript": "^5.9.
|
|
43
|
-
"vitest": "^4.
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.1.12"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=20.0.0"
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"author": "AnonymousOnyx22",
|
|
50
|
-
"homepage": "https://
|
|
50
|
+
"homepage": "https://brightspace-mcp.netlify.app",
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
|
53
53
|
"url": "git+https://github.com/AnonymousOnyx22/brightspace-d2l-mcp.git"
|