brightspace-mcp-server 3.7.12 → 3.7.14
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 +7 -15
- package/build/auth/browser-auth.js +29 -0
- package/build/auth/purdue-sso.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Connects to D2L Brightspace. Automatic login supports Purdue's Microsoft Entra f
|
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
22
|
-
**You need:** [Node.js 20+](https://nodejs.org/) and
|
|
22
|
+
**You need:** [Node.js 20+](https://nodejs.org/) and a native credential store: macOS Keychain, Windows Credential Manager, or Linux Secret Service (requires `secret-tool` and an unlocked keyring — install `libsecret-tools` on Debian/Ubuntu, or your distribution's `secret-tool` package). A container or SSH session without Secret Service can't persist authentication in v2.
|
|
23
23
|
|
|
24
24
|
**Option 1: Let your AI do it**
|
|
25
25
|
|
|
@@ -58,7 +58,7 @@ npx -y brightspace-mcp-server@latest setup --tudelft
|
|
|
58
58
|
|
|
59
59
|
Use your NetID rather than your student email address. The TU Delft flow is headless NetID username and password sign-in only, including automatic re-authentication when the saved session expires; it does not support MFA or any other interactive step. If your account requires one, [open an issue](https://github.com/RohanMuppa/brightspace-mcp-server/issues) — that tenant isn't supported yet.
|
|
60
60
|
|
|
61
|
-
The wizard saves your password in the native credential store and asks how you complete MFA
|
|
61
|
+
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.
|
|
62
62
|
|
|
63
63
|
Any other D2L school: run `setup` without a flag and paste your Brightspace URL (for example `https://yourschool.brightspace.com`).
|
|
64
64
|
|
|
@@ -95,27 +95,19 @@ Claude Desktop uses a separate configuration, which the setup wizard can update
|
|
|
95
95
|
|
|
96
96
|
## Session Expired?
|
|
97
97
|
|
|
98
|
-
There
|
|
98
|
+
There's no login step — asking a question signs you in.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
**Normal days:** tokens renew over HTTPS, and a background browser silently replays your saved Microsoft session (silent SSO) if it lapses.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
**When MFA is asked:** the number to approve shows up right in the tool's response, and sign-in finishes in the background — approve it, call the tool again, and use the newest number if one goes stale. TOTP apps (Google Authenticator, etc.) get prompted via the terminal; visible-browser mode opens a window instead, though automatic recovery during a tool call still runs headless unless you set `D2L_HEADLESS=false`. On Duo, sign-in auto-answers "Is this your device?" with **yes**, since a headless run has nobody to click it — this also makes Duo remember the device, so skip it on shared machines. Set `D2L_DUO_PASSCODE` to swap the push for a typed passcode.
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
If you miss an MFA request, automatic browser authentication pauses for five minutes before trying again. Existing tokens and HTTP token renewal still work. Browser-based SSO also pauses because Microsoft can send another phone prompt during a redirect, even without a password submission. Run this command in a terminal to retry immediately, see a number match, or enter an authenticator code:
|
|
104
|
+
**If it gets stuck:** a missed MFA approval pauses automatic sign-in for 5 minutes. This retries immediately, takes over a stuck sign-in, or asks for a code:
|
|
107
105
|
|
|
108
106
|
```bash
|
|
109
107
|
npx -y brightspace-mcp-server@latest auth
|
|
110
108
|
```
|
|
111
109
|
|
|
112
|
-
Run it from your home folder
|
|
113
|
-
|
|
114
|
-
**MFA at Purdue** commonly uses Microsoft Authenticator number matching (some schools use Duo instead). When a sign-in needs it, the tool call itself returns quickly with the number to enter, rather than sitting silent for up to five minutes — approve it on your phone, then call the tool again; sign-in finishes in the background in the meantime. Google Authenticator and other one-time-code apps work too, with no setting to change: run the auth command above in a terminal and it prompts for the code when your provider asks for one. Pick the visible-browser option during setup only if your identity provider needs interaction the server cannot drive. The MCP also sends authentication progress as logging notifications to clients that display them, useful if you don't see the number in the tool response for some reason.
|
|
115
|
-
|
|
116
|
-
The number in that response can go stale if the request times out or you tap Deny before approving it: the server asks Microsoft for another one and the next tool call reports the new number, so always enter whatever number the most recent response shows. If a background sign-in gets stuck, running the auth command above in a terminal takes it over immediately instead of waiting for it to finish or time out.
|
|
117
|
-
|
|
118
|
-
**On a Duo tenant**, if Duo asks "Is this your device?" before it will send a push, automatic sign-in answers **yes** so the push can go out at all — a headless run has nobody to click it otherwise. That also makes Duo remember the device, which skips its own device check on later logins from this machine. Don't run automatic sign-in on a shared or public computer if you'd rather Duo keep asking. Setting `D2L_DUO_PASSCODE` to any value switches from waiting for a push to typing a code from Duo Mobile's passcode option instead.
|
|
110
|
+
Run it from your home folder — macOS blocks `npx` from Documents, Desktop, or Downloads without Files and Folders permission (`EPERM`). Grant access in System Settings → Privacy & Security → Files and Folders, or run elsewhere.
|
|
119
111
|
|
|
120
112
|
## What You Can Ask About
|
|
121
113
|
|
|
@@ -30,6 +30,11 @@ const SILENT_SSO = {
|
|
|
30
30
|
kmsiCheckbox: "#KmsiCheckboxField",
|
|
31
31
|
kmsiTitle: "Stay signed in?",
|
|
32
32
|
kmsiSubmit: "#idSIButton9",
|
|
33
|
+
// Microsoft's passwordless approval view ("Approve sign in request", or
|
|
34
|
+
// "Request wasn't sent" once a push fails) offers the password instead. The
|
|
35
|
+
// text fallback covers a tenant that renames the id.
|
|
36
|
+
passwordSwitch: "#idA_PWD_SwitchToPassword",
|
|
37
|
+
passwordSwitchText: "Use your password instead",
|
|
33
38
|
};
|
|
34
39
|
export class BrowserAuthTransportError extends Error {
|
|
35
40
|
code = "AUTH_TRANSPORT";
|
|
@@ -338,6 +343,7 @@ export class BrowserAuth {
|
|
|
338
343
|
const deadline = Date.now() + SILENT_SSO_TIMEOUT_MS;
|
|
339
344
|
let accountHintAttempted = false;
|
|
340
345
|
let passwordPromptPolls = 0;
|
|
346
|
+
let passwordSwitchAttempted = false;
|
|
341
347
|
do {
|
|
342
348
|
if (await this.hasLiveSession(page))
|
|
343
349
|
return true;
|
|
@@ -359,6 +365,22 @@ export class BrowserAuth {
|
|
|
359
365
|
log("INFO", "The identity provider requires a login challenge");
|
|
360
366
|
return false;
|
|
361
367
|
}
|
|
368
|
+
const passwordSwitch = await this.passwordSignInOption(page);
|
|
369
|
+
if (passwordSwitch) {
|
|
370
|
+
// The approval view has no field or MFA title this poll recognizes, so
|
|
371
|
+
// without leaving it the poll would wait out its whole budget. Switch
|
|
372
|
+
// once when a password is saved; the password page then settles the
|
|
373
|
+
// poll like any other credential prompt.
|
|
374
|
+
if (!passwordSwitchAttempted && this.ssoFlow.hasCredentials()) {
|
|
375
|
+
passwordSwitchAttempted = true;
|
|
376
|
+
await passwordSwitch.click().catch(() => { });
|
|
377
|
+
log("DEBUG", "Chose the password sign-in option over passwordless approval");
|
|
378
|
+
await page.waitForTimeout(SILENT_SSO_POLL_MS);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
log("INFO", "The identity provider requires a password sign-in");
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
362
384
|
if (await this.isAnyOnScreen(page, SILENT_SSO.credentialFields)) {
|
|
363
385
|
passwordPromptPolls += 1;
|
|
364
386
|
// Entra can briefly expose a password-shaped control while its email
|
|
@@ -390,6 +412,13 @@ export class BrowserAuth {
|
|
|
390
412
|
return await this.isAnyOnScreen(page, SILENT_SSO.emailFields) ||
|
|
391
413
|
await this.isAnyOnScreen(page, SILENT_SSO.credentialFields);
|
|
392
414
|
}
|
|
415
|
+
/** The visible "Use your password instead" control, when Microsoft offers one. */
|
|
416
|
+
async passwordSignInOption(page) {
|
|
417
|
+
if (await this.isOnScreen(page, SILENT_SSO.passwordSwitch))
|
|
418
|
+
return page.locator(SILENT_SSO.passwordSwitch).first();
|
|
419
|
+
const byText = page.getByText(SILENT_SSO.passwordSwitchText).first();
|
|
420
|
+
return await byText.isVisible().catch(() => false) ? byText : null;
|
|
421
|
+
}
|
|
393
422
|
/** One definition of the MFA pages the shared authentication loop supports. */
|
|
394
423
|
async hasMfaChallenge(page) {
|
|
395
424
|
return isDuoPrompt(page) || await this.isAnyOnScreen(page, SILENT_SSO.mfaChallenges);
|
package/build/auth/purdue-sso.js
CHANGED
|
@@ -174,7 +174,11 @@ export class PurdueSSOFlow {
|
|
|
174
174
|
// and then blames a missing password field.
|
|
175
175
|
const hintAccepted = this.accountHintSubmitted && !await this.anyVisible(page, EMAIL_SELECTORS);
|
|
176
176
|
this.accountHintSubmitted = false;
|
|
177
|
-
|
|
177
|
+
// Microsoft can also know the account without a hint from this login: a
|
|
178
|
+
// remembered account's passwordless approval view, once awaitSilentSSO
|
|
179
|
+
// takes "Use your password instead", leads to a password page with no
|
|
180
|
+
// username field at all.
|
|
181
|
+
if (!hintAccepted && !await this.isPasswordOnlyPage(page)) {
|
|
178
182
|
const email = signInName(this.config.username, this.config.baseUrl);
|
|
179
183
|
if (!await this.fillWhenReady(page, EMAIL_SELECTORS, email)) {
|
|
180
184
|
throw new UnsupportedAuthenticationError("The identity provider's username field did not appear. Automatic sign-in cannot continue.");
|
|
@@ -253,6 +257,10 @@ export class PurdueSSOFlow {
|
|
|
253
257
|
await page.waitForTimeout(FIELD_POLL_MS);
|
|
254
258
|
return await this.anyVisible(page, PASSWORD_SELECTORS);
|
|
255
259
|
}
|
|
260
|
+
/** A stable password field with no username field beside it. */
|
|
261
|
+
async isPasswordOnlyPage(page) {
|
|
262
|
+
return await this.hasCoVisiblePassword(page) && !await this.anyVisible(page, EMAIL_SELECTORS);
|
|
263
|
+
}
|
|
256
264
|
async hasPostCredentialChallenge(page) {
|
|
257
265
|
return this.duoMfa.isChallenge(page) || await this.anyVisible(page, [
|
|
258
266
|
NUMBER_MATCH_SELECTOR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.14",
|
|
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",
|