brightspace-mcp-server 5.1.39 → 5.1.40
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/mun-sso.js +128 -0
- package/build/auth/sso-flow.js +4 -0
- package/build/setup/presets.js +6 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -30,14 +30,14 @@ Works with Claude Desktop, Claude Code, Cursor, ChatGPT Desktop, Windsurf, and a
|
|
|
30
30
|
**On a Mac:** when Keychain asks whether `node` may use the saved Brightspace password, choose **Always Allow**, not just Allow. That lets later sign-ins (the `auth` command and questions from your AI app) read it without asking again.
|
|
31
31
|
3. **Restart your AI app and ask it something** — the first question signs you in. If your phone asks you to approve a sign-in, approve it and ask again.
|
|
32
32
|
|
|
33
|
-
At one of these schools, add the flag and skip typing the address: `--purdue`, `--suny`, `--western`, `--tudelft`, `--cuny`, `--leiden`, `--mcgill`, `--ngeeann`, `--javeriana`.
|
|
33
|
+
At one of these schools, add the flag and skip typing the address: `--purdue`, `--suny`, `--western`, `--tudelft`, `--cuny`, `--leiden`, `--mcgill`, `--mun`, `--ngeeann`, `--javeriana`.
|
|
34
34
|
|
|
35
35
|
**Rather have the AI install it for you?** Paste this into Claude Code, Cursor, Windsurf, Copilot, or Codex:
|
|
36
36
|
|
|
37
37
|
```
|
|
38
38
|
Install brightspace-mcp-server for me by following
|
|
39
39
|
https://github.com/RohanMuppa/brightspace-mcp-server/blob/main/LLMs.md
|
|
40
|
-
(use --purdue at Purdue, --suny at SUNY, --tudelft at TU Delft, --cuny at CUNY, --leiden at Leiden, --mcgill at McGill, --ngeeann at Ngee Ann Polytechnic, or --javeriana at Javeriana Cali).
|
|
40
|
+
(use --purdue at Purdue, --suny at SUNY, --tudelft at TU Delft, --cuny at CUNY, --leiden at Leiden, --mcgill at McGill, --mun at Memorial University, --ngeeann at Ngee Ann Polytechnic, or --javeriana at Javeriana Cali).
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The AI does everything it can by itself: it checks your computer, downloads the browser sign-in uses, and connects your AI apps. Then it gives you one command to run in a terminal. That command asks for your password and waits for you to approve the sign-in on your phone. That's the only part that needs you. Afterwards the AI checks that it can see your courses.
|
|
@@ -57,6 +57,7 @@ If your school uses D2L Brightspace, yes. These have automatic sign-in built in:
|
|
|
57
57
|
| CUNY | CUNY Login, authenticator code each time | `--cuny` |
|
|
58
58
|
| Leiden University | SURFconext → Microsoft | `--leiden` |
|
|
59
59
|
| McGill University | Microsoft Entra | `--mcgill` |
|
|
60
|
+
| Memorial University of Newfoundland | MUN Login (CAS, no two-factor when tested) | `--mun` |
|
|
60
61
|
| Javeriana Cali | OneGate (password + authenticator code) | `--javeriana` |
|
|
61
62
|
| Ngee Ann Polytechnic | Microsoft Entra | `--ngeeann` |
|
|
62
63
|
| Any other D2L school | Paste your Brightspace address. If the login page isn't recognized, pick option 2 at setup's sign-in question so the page opens for you to finish by hand | none |
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Memorial University of Newfoundland (MUN) login entry point. Licensed under MIT; see LICENSE. */
|
|
2
|
+
import { log } from "../utils/logger.js";
|
|
3
|
+
import { SETUP_COMMAND } from "../utils/commands.js";
|
|
4
|
+
import { BrowserAuthError } from "../utils/errors.js";
|
|
5
|
+
import { PurdueSSOFlow } from "./purdue-sso.js";
|
|
6
|
+
import { UnsupportedAuthenticationError } from "./sso-flow.js";
|
|
7
|
+
const MUN_HOST = "online.mun.ca";
|
|
8
|
+
const CAS_HOST = "login.mun.ca";
|
|
9
|
+
const ENTRA_HOST = "login.microsoftonline.com";
|
|
10
|
+
const SELECTORS = {
|
|
11
|
+
loginButton: "#mun-login",
|
|
12
|
+
username: "input#username",
|
|
13
|
+
password: "input#password",
|
|
14
|
+
submit: 'button[name="submitBtn"]',
|
|
15
|
+
};
|
|
16
|
+
const POLL_MS = 500;
|
|
17
|
+
const HEADLESS_TIMEOUT_MS = 60_000;
|
|
18
|
+
/** A visible window may have a person finishing a page this flow does not know. */
|
|
19
|
+
const VISIBLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
20
|
+
export function isMunBrightspace(baseUrl) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(baseUrl).hostname.toLowerCase() === MUN_HOST;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function secureHost(url) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
return parsed.protocol === "https:" ? parsed.hostname.toLowerCase() : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* MUN's Brightspace lands on its own page at /d2l/loginh/, whose "MUN Login"
|
|
39
|
+
* button leads to MUN's CAS server, not to SAML or Microsoft:
|
|
40
|
+
*
|
|
41
|
+
* /d2l/loginh/ -> /d2l/custom/cas -> login.mun.ca/cas/login -> /d2l/home
|
|
42
|
+
*
|
|
43
|
+
* The default flow never clicks that button, and CAS's plain `username` field
|
|
44
|
+
* matches none of its username selectors. Seen live on 2026-11-05.
|
|
45
|
+
*
|
|
46
|
+
* If MUN ever sends the login on to Microsoft, the shared flow takes over from
|
|
47
|
+
* there, which answers "Stay signed in?", "Don't ask again" and code prompts.
|
|
48
|
+
*/
|
|
49
|
+
export class MunSSOFlow {
|
|
50
|
+
common;
|
|
51
|
+
config;
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.common = new PurdueSSOFlow(config);
|
|
55
|
+
}
|
|
56
|
+
hasCredentials() {
|
|
57
|
+
return this.common.hasCredentials();
|
|
58
|
+
}
|
|
59
|
+
rememberMfaResult() {
|
|
60
|
+
return this.common.rememberMfaResult();
|
|
61
|
+
}
|
|
62
|
+
/** Called on every poll, so a button that has not rendered yet is simply tried again. */
|
|
63
|
+
async prepareLogin(page) {
|
|
64
|
+
if (secureHost(page.url()) !== MUN_HOST || !new URL(page.url()).pathname.startsWith("/d2l/login"))
|
|
65
|
+
return;
|
|
66
|
+
const button = page.locator(SELECTORS.loginButton).first();
|
|
67
|
+
if (await button.isVisible().catch(() => false)) {
|
|
68
|
+
log("DEBUG", "Clicked the MUN Login button");
|
|
69
|
+
await button.click().catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async login(page) {
|
|
73
|
+
const deadline = Date.now() + (this.config.headless === false ? VISIBLE_TIMEOUT_MS : HEADLESS_TIMEOUT_MS);
|
|
74
|
+
let submitted = false;
|
|
75
|
+
let navigatedAfterSubmit = false;
|
|
76
|
+
const onNavigated = (frame) => {
|
|
77
|
+
if (submitted && frame === page.mainFrame())
|
|
78
|
+
navigatedAfterSubmit = true;
|
|
79
|
+
};
|
|
80
|
+
page.on("framenavigated", onNavigated);
|
|
81
|
+
try {
|
|
82
|
+
while (Date.now() < deadline) {
|
|
83
|
+
const host = secureHost(page.url());
|
|
84
|
+
if (host === MUN_HOST && /^\/d2l\/home(?:\/|$)/.test(new URL(page.url()).pathname))
|
|
85
|
+
return true;
|
|
86
|
+
if (host === ENTRA_HOST)
|
|
87
|
+
return await this.common.login(page);
|
|
88
|
+
await this.prepareLogin(page);
|
|
89
|
+
// Never type the password into anything but MUN's own login server.
|
|
90
|
+
if (host === CAS_HOST && await page.locator(SELECTORS.password).first().isVisible().catch(() => false)) {
|
|
91
|
+
// The form coming back means CAS said no. Sending it again would only
|
|
92
|
+
// count toward a lockout.
|
|
93
|
+
if (navigatedAfterSubmit) {
|
|
94
|
+
throw new BrowserAuthError(`MUN Login rejected the saved username or password. Run \`${SETUP_COMMAND} --mun\` to update them.`, "credentials");
|
|
95
|
+
}
|
|
96
|
+
if (!submitted) {
|
|
97
|
+
submitted = true;
|
|
98
|
+
await this.enterCredentials(page);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
await page.waitForTimeout(POLL_MS);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof BrowserAuthError)
|
|
106
|
+
throw error;
|
|
107
|
+
throw new UnsupportedAuthenticationError("MUN Login could not complete automatic sign-in.", error);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
page.off("framenavigated", onNavigated);
|
|
111
|
+
}
|
|
112
|
+
throw new UnsupportedAuthenticationError("MUN sign-in did not reach Brightspace. MUN may have changed its login pages or asked for a second factor; set D2L_HEADLESS=false to finish sign-in in the browser.");
|
|
113
|
+
}
|
|
114
|
+
async enterCredentials(page) {
|
|
115
|
+
if (!this.config.username)
|
|
116
|
+
throw new BrowserAuthError("Username is required for SSO login", "credentials");
|
|
117
|
+
if (!this.config.password)
|
|
118
|
+
throw new BrowserAuthError("Password is required for SSO login", "credentials");
|
|
119
|
+
log("INFO", "Entering MUN Login credentials");
|
|
120
|
+
await page.locator(SELECTORS.username).first().fill(this.config.username);
|
|
121
|
+
// Re-check right before the password goes in, in case the page moved since the poll.
|
|
122
|
+
if (secureHost(page.url()) !== CAS_HOST)
|
|
123
|
+
return;
|
|
124
|
+
await page.locator(SELECTORS.password).first().fill(this.config.password);
|
|
125
|
+
// CAS answers with a full navigation, which can detach the button.
|
|
126
|
+
await page.locator(SELECTORS.submit).first().click().catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
}
|
package/build/auth/sso-flow.js
CHANGED
|
@@ -11,6 +11,7 @@ import { CunySSOFlow, isCunyBrightspace } from "./cuny-sso.js";
|
|
|
11
11
|
import { LeidenSSOFlow, isLeidenBrightspace } from "./leiden-sso.js";
|
|
12
12
|
import { McgillSSOFlow, isMcgillBrightspace } from "./mcgill-sso.js";
|
|
13
13
|
import { JaverianaSSOFlow, isJaverianaBrightspace } from "./javeriana-sso.js";
|
|
14
|
+
import { MunSSOFlow, isMunBrightspace } from "./mun-sso.js";
|
|
14
15
|
import { BrowserAuthError } from "../utils/errors.js";
|
|
15
16
|
import { AUTH_COMMAND } from "../utils/commands.js";
|
|
16
17
|
export class UnsupportedAuthenticationError extends BrowserAuthError {
|
|
@@ -90,5 +91,8 @@ export function createSSOFlow(config, requestMfaCode, onMfaChallenge, onAutomati
|
|
|
90
91
|
if (isJaverianaBrightspace(config.baseUrl)) {
|
|
91
92
|
return new JaverianaSSOFlow(credentials);
|
|
92
93
|
}
|
|
94
|
+
if (isMunBrightspace(config.baseUrl)) {
|
|
95
|
+
return new MunSSOFlow(credentials);
|
|
96
|
+
}
|
|
93
97
|
return new PurdueSSOFlow(credentials);
|
|
94
98
|
}
|
package/build/setup/presets.js
CHANGED
|
@@ -60,6 +60,12 @@ export const SCHOOL_PRESETS = {
|
|
|
60
60
|
mfaNote: "Approve the sign-in request from your MFA app.",
|
|
61
61
|
usernameHint: "Use your full sign-in address if your McGill account requires it.",
|
|
62
62
|
},
|
|
63
|
+
mun: {
|
|
64
|
+
name: "Memorial University of Newfoundland",
|
|
65
|
+
baseUrl: "https://online.mun.ca",
|
|
66
|
+
usernameLabel: "MUN Login ID",
|
|
67
|
+
mfaNote: "MUN Login normally runs headlessly without MFA.",
|
|
68
|
+
},
|
|
63
69
|
javeriana: {
|
|
64
70
|
name: "Pontificia Universidad Javeriana Cali",
|
|
65
71
|
baseUrl: "https://auladigital.javerianacali.edu.co",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.40",
|
|
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",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"cuny",
|
|
93
93
|
"leiden",
|
|
94
94
|
"mcgill",
|
|
95
|
+
"mun",
|
|
95
96
|
"javeriana",
|
|
96
97
|
"ngee-ann"
|
|
97
98
|
]
|