brightspace-mcp-server 3.8.12 → 3.8.13
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 +10 -2
- package/build/auth/browser-auth.js +3 -1
- package/build/auth/index.js +1 -0
- package/build/auth/leiden-sso.js +100 -0
- package/build/auth/sso-flow.js +4 -0
- package/build/setup.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Talk to your Brightspace courses with AI. Ask about grades, due dates, quizzes,
|
|
|
6
6
|
|
|
7
7
|
This is an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that connects your AI to D2L Brightspace so it can pull your grades, assignments, syllabus, and course content on demand.
|
|
8
8
|
|
|
9
|
-
Connects to D2L Brightspace. Automatic login supports Purdue's Microsoft Entra flow, SUNY campus selection, Western University, TU Delft NetID via SURFconext, and
|
|
9
|
+
Connects to D2L Brightspace. Automatic login supports Purdue's Microsoft Entra flow, SUNY campus selection, Western University, TU Delft NetID via SURFconext, CUNY Login, and Leiden University via SURFconext and Microsoft Entra. Other schools need a compatible automated sign-in flow; unsupported login pages return an actionable error.
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<img src="https://raw.githubusercontent.com/RohanMuppa/brightspace-mcp-server/main/docs/how-it-works.svg" alt="Architecture diagram" width="100%">
|
|
@@ -28,7 +28,7 @@ Paste this into Claude Code, Cursor, Windsurf, Copilot, Codex, or any AI coding
|
|
|
28
28
|
```
|
|
29
29
|
Install brightspace-mcp-server for me by following
|
|
30
30
|
https://github.com/RohanMuppa/brightspace-mcp-server/blob/main/LLMs.md
|
|
31
|
-
(use --purdue at Purdue, --suny at SUNY, --tudelft at TU Delft, or --
|
|
31
|
+
(use --purdue at Purdue, --suny at SUNY, --tudelft at TU Delft, --cuny at CUNY, or --leiden at Leiden).
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
**Option 2: Run it yourself**
|
|
@@ -68,6 +68,14 @@ every full sign-in, so when your Brightspace session ends, run
|
|
|
68
68
|
npx -y brightspace-mcp-server@latest setup --cuny
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
Leiden University students can use `--leiden`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx -y brightspace-mcp-server@latest setup --leiden
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Sign in with your full Microsoft address (for example `s1234567@vuw.leidenuniv.nl`). The flow picks Leiden University (Entra) on SURFconext's account page, then uses the same Microsoft sign-in as Purdue. Leiden normally asks for a code from your authenticator app, which `auth` prompts for in the terminal.
|
|
78
|
+
|
|
71
79
|
The wizard saves your password in the native credential store and asks how you'll complete MFA: wait for approval or number matching, enter a terminal code from Google Authenticator or another app, or use a visible browser for other interactive methods. It can also configure Claude Desktop, Cursor, Codex Desktop and CLI, and Claude Code when installed — restart your AI client when it finishes.
|
|
72
80
|
|
|
73
81
|
Any other D2L school: run `setup` without a flag and paste your Brightspace URL (for example `https://yourschool.brightspace.com`).
|
|
@@ -512,8 +512,10 @@ export class BrowserAuth {
|
|
|
512
512
|
}
|
|
513
513
|
return;
|
|
514
514
|
}
|
|
515
|
-
if (!url.includes("login.microsoftonline.com"))
|
|
515
|
+
if (!url.includes("login.microsoftonline.com")) {
|
|
516
|
+
await this.ssoFlow.selectIdentityProvider?.(page);
|
|
516
517
|
return;
|
|
518
|
+
}
|
|
517
519
|
// The page must PROVE it is the "Stay signed in?" page before this click:
|
|
518
520
|
// #idSIButton9 is Microsoft's id for the primary button on EVERY sign-in
|
|
519
521
|
// page, "Next" on account name and "Sign in" on password, so clicking it
|
package/build/auth/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { PurdueSSOFlow } from "./purdue-sso.js";
|
|
|
8
8
|
export { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
|
|
9
9
|
export { TUDelftSSOFlow, isTUDelftBrightspace } from "./tudelft-sso.js";
|
|
10
10
|
export { CunySSOFlow, isCunyBrightspace } from "./cuny-sso.js";
|
|
11
|
+
export { LeidenSSOFlow, isLeidenBrightspace } from "./leiden-sso.js";
|
|
11
12
|
export { createSSOFlow } from "./sso-flow.js";
|
|
12
13
|
export { TokenManager } from "./token-manager.js";
|
|
13
14
|
export { SessionStore } from "./session-store.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Leiden University login via SURFconext and Microsoft Entra. Licensed under MIT; see LICENSE. */
|
|
2
|
+
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
3
|
+
import { UnsupportedAuthenticationError } from "./sso-flow.js";
|
|
4
|
+
import { log } from "../utils/logger.js";
|
|
5
|
+
const LEIDEN_HOST = "brightspace.universiteitleiden.nl";
|
|
6
|
+
const SURFCONEXT_HOST = "engine.surfconext.nl";
|
|
7
|
+
const ENTRA_HOST = "login.microsoftonline.com";
|
|
8
|
+
// SURFconext lists eduID first and LUMC next to Leiden, so the account is
|
|
9
|
+
// picked by entity ID rather than by position or a "Leiden" text match.
|
|
10
|
+
export const LEIDEN_ENTRA_ENTITY_ID = "https://sts.windows.net/ca2a7f76-dbd7-4ec0-9108-6b3d524fb7c8/";
|
|
11
|
+
// The list wrapper never reports as visible, so the search box marks the picker.
|
|
12
|
+
const WAYF_SEARCH = "#wayf_search";
|
|
13
|
+
const LEIDEN_WAYF_ENTRY = `.wayf__idp[data-entityid="${LEIDEN_ENTRA_ENTITY_ID}"]`;
|
|
14
|
+
// SURFconext's picker can take a poll or two to render the account list
|
|
15
|
+
// (the search box itself appears first), so a miss is not treated as
|
|
16
|
+
// authoritative until it has survived a few polls in a row.
|
|
17
|
+
const MAX_WAYF_ENTRY_MISSES = 3;
|
|
18
|
+
export function isLeidenBrightspace(baseUrl) {
|
|
19
|
+
try {
|
|
20
|
+
return new URL(baseUrl).hostname.toLowerCase() === LEIDEN_HOST;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isSurfconext(rawUrl) {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(rawUrl);
|
|
29
|
+
return url.protocol === "https:" && url.hostname.toLowerCase() === SURFCONEXT_HOST;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isMicrosoftEntra(rawUrl) {
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(rawUrl);
|
|
38
|
+
return url.protocol === "https:" && url.hostname.toLowerCase() === ENTRA_HOST;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Brightspace sends Leiden users straight to SURFconext's account picker.
|
|
46
|
+
* Choosing the Entra entry there is the only Leiden-specific step; the
|
|
47
|
+
* Microsoft sign-in after it is the shared flow.
|
|
48
|
+
*/
|
|
49
|
+
export class LeidenSSOFlow {
|
|
50
|
+
common;
|
|
51
|
+
wayfEntryMisses = 0;
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.common = new PurdueSSOFlow(config);
|
|
54
|
+
}
|
|
55
|
+
hasCredentials() {
|
|
56
|
+
return this.common.hasCredentials();
|
|
57
|
+
}
|
|
58
|
+
rememberMfaResult() {
|
|
59
|
+
return this.common.rememberMfaResult();
|
|
60
|
+
}
|
|
61
|
+
async selectIdentityProvider(page) {
|
|
62
|
+
await this.chooseLeidenAccount(page);
|
|
63
|
+
}
|
|
64
|
+
async identifyAccount(page) {
|
|
65
|
+
return this.common.identifyAccount(page);
|
|
66
|
+
}
|
|
67
|
+
async login(page) {
|
|
68
|
+
await this.chooseLeidenAccount(page);
|
|
69
|
+
// The Microsoft sign-in flow below is shared with Purdue and fills
|
|
70
|
+
// whatever email/password fields it finds without checking where it is.
|
|
71
|
+
// Refuse to hand it the page at all unless SURFconext actually landed on
|
|
72
|
+
// Microsoft Entra: a redirect to a lookalike host must never reach the
|
|
73
|
+
// point where credentials get typed.
|
|
74
|
+
if (!isMicrosoftEntra(page.url())) {
|
|
75
|
+
throw new UnsupportedAuthenticationError("Leiden sign-in reached an unsupported identity-provider page. No credentials were entered there.");
|
|
76
|
+
}
|
|
77
|
+
return this.common.login(page);
|
|
78
|
+
}
|
|
79
|
+
async chooseLeidenAccount(page) {
|
|
80
|
+
if (!isSurfconext(page.url()))
|
|
81
|
+
return;
|
|
82
|
+
// The redirect back to Brightspace also passes through SURFconext.
|
|
83
|
+
if (!await page.locator(WAYF_SEARCH).first().isVisible().catch(() => false))
|
|
84
|
+
return;
|
|
85
|
+
const entry = page.locator(LEIDEN_WAYF_ENTRY).first();
|
|
86
|
+
if (!await entry.isVisible().catch(() => false)) {
|
|
87
|
+
this.wayfEntryMisses++;
|
|
88
|
+
// A lazily rendered account list can miss the first poll or two on a
|
|
89
|
+
// working account; only a miss that survives several polls means
|
|
90
|
+
// SURFconext genuinely stopped offering the Leiden entry.
|
|
91
|
+
if (this.wayfEntryMisses > MAX_WAYF_ENTRY_MISSES) {
|
|
92
|
+
throw new UnsupportedAuthenticationError("SURFconext no longer offers the Leiden University (Entra) account. Sign in with D2L_HEADLESS=false and pick the account manually.");
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.wayfEntryMisses = 0;
|
|
97
|
+
log("INFO", "Choosing Leiden University (Entra) on SURFconext");
|
|
98
|
+
await entry.click();
|
|
99
|
+
}
|
|
100
|
+
}
|
package/build/auth/sso-flow.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SunySSOFlow, isSunyBrightspace } from "./suny-sso.js";
|
|
|
8
8
|
import { WesternSSOFlow, isWesternBrightspace } from "./western-sso.js";
|
|
9
9
|
import { TUDelftSSOFlow, isTUDelftBrightspace } from "./tudelft-sso.js";
|
|
10
10
|
import { CunySSOFlow, isCunyBrightspace } from "./cuny-sso.js";
|
|
11
|
+
import { LeidenSSOFlow, isLeidenBrightspace } from "./leiden-sso.js";
|
|
11
12
|
import { BrowserAuthError } from "../utils/errors.js";
|
|
12
13
|
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
13
14
|
export class UnsupportedAuthenticationError extends BrowserAuthError {
|
|
@@ -50,6 +51,9 @@ export function createSSOFlow(config, requestMfaCode, onMfaChallenge) {
|
|
|
50
51
|
if (isTUDelftBrightspace(config.baseUrl)) {
|
|
51
52
|
return new TUDelftSSOFlow(credentials);
|
|
52
53
|
}
|
|
54
|
+
if (isLeidenBrightspace(config.baseUrl)) {
|
|
55
|
+
return new LeidenSSOFlow(credentials);
|
|
56
|
+
}
|
|
53
57
|
if (isSunyBrightspace(config.baseUrl)) {
|
|
54
58
|
return new SunySSOFlow({ ...credentials, campus: config.campus });
|
|
55
59
|
}
|
package/build/setup.js
CHANGED
|
@@ -31,6 +31,13 @@ export const SCHOOL_PRESETS = {
|
|
|
31
31
|
mfaNote: "NetID sign-in normally runs headlessly without MFA.",
|
|
32
32
|
usernameHint: "Use your NetID, not your student email address.",
|
|
33
33
|
},
|
|
34
|
+
leiden: {
|
|
35
|
+
name: "Leiden University",
|
|
36
|
+
baseUrl: "https://brightspace.universiteitleiden.nl",
|
|
37
|
+
usernameLabel: "Leiden University username",
|
|
38
|
+
mfaNote: "Leiden usually asks for a code from your authenticator app, which you can type here.",
|
|
39
|
+
usernameHint: "Use the full address you sign in to Microsoft with, e.g. s1234567@vuw.leidenuniv.nl.",
|
|
40
|
+
},
|
|
34
41
|
purdue: {
|
|
35
42
|
name: "Purdue University",
|
|
36
43
|
baseUrl: "https://purdue.brightspace.com",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.13",
|
|
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",
|